Technology Focus: 10x Genomics Xenium#

To follow along with this tutorial, you can download the data already in SpatialData format from here.

Description of prior dataset acquisition and segmentation#

Dataset Acquisition (10x Genomics Xenium)#

For this tutorial, we used data from the 10x Genomics Xenium Prime Breast Cancer FFPE dataset. We downloaded the data using:

curl -O https://s3-us-west-2.amazonaws.com/10x.files/samples/xenium/3.0.0/
Xenium_Prime_Breast_Cancer_FFPE/Xenium_Prime_Breast_Cancer_FFPE_outs.zip

Subsetting the Field of View#

We subset the dataset using the following bounding box (X, Y coordinates):

2631.31, 8503.97
4150.72, 8503.97
4150.72, 10298.85
2631.31, 10298.85

Cell Segmentation Methods#

We applied two segmentation approaches for comparison to the Xenium Multi-Modal Cell Segmentation:

  1. BIDCellSydneyBioX/BIDCell

  2. Proseg (v2.0.5) — dcjones/proseg

Reference scRNA-seq Dataset for Training & QC#

For BIDCell training, as well as supervised quality control (see below in this tutorial), we used the Janesick et al. scRNA-seq breast cancer dataset following:

Download commands:

wget -O count_matrix.h5 "https://www.ncbi.nlm.nih.gov/geo/download/
?acc=GSM7782698&format=file&file=GSM7782698%5Fcount%5Fraw%5Ffeature%5Fbc%5Fmatrix%2Eh5"
wget -O Cell_Barcode_Type_Matrices.xlsx "https://zenodo.org/records/10076046/files/
Cell_Barcode_Type_Matrices.xlsx?download=1"

Segmentation Parameters#

  • Proseg

    • Used default parameters.

  • BIDCell

    • Annotated elongated cell types:

      • endo

      • stromal

      • myoepi

      • perivas

    • Additional settings:

      • test_epoch = 4

      • test_steps = 100

    • All other parameters left at default.

Import into SpatialData#

After segmentation, all outputs were imported into SpatialData objects. For a demonstration on how to get data into the right format, please refer to this tutorial. These processed SpatialData objects are also uploaded to the tutorial repository so users can directly run the workflow without repeating the preprocessing steps.

Read and crop SpatialData objects#

[1]:

import anndata as ad import matplotlib.patches as patches import matplotlib.pyplot as plt import numpy as np import pandas as pd import scanpy as sc import seaborn as sns import spatialdata as sd import spatialdata_plot # noqa import segtraq segtraq.settings.n_jobs = -1 # Use all available CPU cores # warnings.filterwarnings(action="ignore")
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
[2]:
sdata_xenium_ws = sd.read_zarr("../../data/xenium_5K_data/xenium.zarr")
sdata_bidcell_ws = sd.read_zarr("../../data/xenium_5K_data/bidcell.zarr")
sdata_proseg_ws = sd.read_zarr("../../data/xenium_5K_data/proseg2.zarr")
no parent found for <ome_zarr.reader.Label object at 0x7fff7dd1a120>: None
no parent found for <ome_zarr.reader.Label object at 0x7fff7dde1d10>: None

The SpatialData objects can be subset via spatialdata.query.bounding_box().

[3]:
bb_xmin = 800
bb_ymin = 1150
bb_w = 200
bb_h = 300
bb_xmax = bb_xmin + bb_w
bb_ymax = bb_ymin + bb_h
[4]:
f, ax = plt.subplots(figsize=(5, 5))
sdata_xenium_ws.pl.render_shapes("cell_boundaries").pl.show(ax=ax)
rect = patches.Rectangle((bb_xmin, bb_ymin), bb_w, bb_h, linewidth=5, edgecolor="red", facecolor="none")
ax.add_patch(rect)
[4]:
<matplotlib.patches.Rectangle at 0x7fff370e9d10>
../_images/notebooks_10x_xenium_focus_7_1.png
[5]:
def crop(ws):
    return ws.query.bounding_box(
        axes=["x", "y"],
        min_coordinate=[bb_xmin, bb_ymin],
        max_coordinate=[bb_xmax, bb_ymax],
        target_coordinate_system="global",
    )


sdata_xenium = crop(sdata_xenium_ws)
sdata_bidcell = crop(sdata_bidcell_ws)
sdata_proseg = crop(sdata_proseg_ws)

Initialize SegTraQ objects#

Next, we initialize SegTraQ objects, the core interface for computing SegTraQ metrics. During initialization, all inputs are are validated via validate_spatialdata(). Don’t worry if you do not know which parameters you need up front; just put in your spatialdata object, and SegTraQ will tell you which arguments are wrong/missing.

The four segmentation methods apply different filters to the points prior to segmentation. Proseg and BIDCell remove control probes (e.g. NegControlProbe, Intergenic_Region) before segmentation. In addition, Proseg and BIDCell exclude transcripts with a quality value (qv, the Xenium per-transcript decoding quality score) < 20.

For a fair comparison of metrics, like the percentage of unassigned transcripts, across methods, we subset the transcripts to include only gene-specific probes with qv ≥ 20. This is also compatible with the preprocessing for the cell feature matrix by 10x Genomics.

[6]:
st_xenium = segtraq.SegTraQ(
    sdata_xenium, images_key="image", tables_centroid_x_key="x_centroid", tables_centroid_y_key="y_centroid"
)
st_bidcell = segtraq.SegTraQ(
    sdata_bidcell,
    images_key="image",
    points_background_id=0,
    tables_area_key=None,
    tables_centroid_x_key="cell_centroid_x",
    tables_centroid_y_key="cell_centroid_y",
)
st_proseg = segtraq.SegTraQ(
    sdata_proseg,
    images_key="image",
    points_cell_id_key="assignment",
    points_background_id=2**32 - 1,
    points_gene_key="gene",
    tables_area_key=None,
    tables_cell_id_key="cell",
    shapes_cell_id_key="cell",
    tables_centroid_x_key="centroid_x",
    tables_centroid_y_key="centroid_y",
)
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/utils.py:1854: UserWarning: Duplicate IDs detected in index 'cell_id' for shapes 'nucleus_boundaries'. Resetting and renaming index to `segtraq_id` to ensure uniqueness.
  nucleus_shapes = _ensure_index(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:190: UserWarning: Filtering control probes matching prefixes ('NegControlProbe_', 'antisense_', 'NegControlCodeword', 'BLANK_', 'Blank-', 'NegPrb', 'DeprecatedCodeword_', 'UnassignedCodeword_', 'Intergenic_Region_') and transcripts with qv < 20 from transcript points in-place; control probes are also removed from the expression table if present. Configure `min_qv`, `control_prefixes`, and `inplace` via `filter_kwargs`.
  sdata_new = _filter_control_and_low_quality_transcripts(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:190: RuntimeWarning: Gene sets differ between assigned transcript points and the expression table. 0 genes occur only in points (e.g. []), and 455 occur only in the table (e.g. ['CARD9', 'CDH22', 'VAMP5', 'POU2F3', 'ZMYND10']). Genes only occuring in the table can be introduced by cropping (`SpatialData`), but differences may also be due to the filtering used to generate the transcript data and expression matrix. Please check that `filter_kwargs`, particularly `min_qv` and `control_prefixes`, are consistent with the filtering used to generate the expression table.
  sdata_new = _filter_control_and_low_quality_transcripts(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:190: RuntimeWarning: Cell IDs differ between assigned transcript points and the expression table. 0 cells occur only in points (e.g. []), and 6 occur only in the table (e.g. ['aphffbmb-1', 'aphemnlm-1', 'iceeeemj-1', 'oemhhohn-1', 'apnfcbpf-1']). Cells only occuring in the table can reflect zero-count cells, but differences may also be due to the filtering used to generate the transcript data and expression matrix. Please check that `filter_kwargs`, particularly `min_qv` and `control_prefixes`, are consistent with the filtering used to generate the expression table.
  sdata_new = _filter_control_and_low_quality_transcripts(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:162: RuntimeWarning: No area column specified for tables. Area will be automatically computed from shapes.
  validate_spatialdata(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:190: UserWarning: Filtering control probes matching prefixes ('NegControlProbe_', 'antisense_', 'NegControlCodeword', 'BLANK_', 'Blank-', 'NegPrb', 'DeprecatedCodeword_', 'UnassignedCodeword_', 'Intergenic_Region_') and transcripts with qv < 20 from transcript points in-place; control probes are also removed from the expression table if present. Configure `min_qv`, `control_prefixes`, and `inplace` via `filter_kwargs`.
  sdata_new = _filter_control_and_low_quality_transcripts(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:190: RuntimeWarning: Gene sets differ between assigned transcript points and the expression table. 0 genes occur only in points (e.g. []), and 438 occur only in the table (e.g. ['CARD9', 'CDH22', 'VAMP5', 'POU2F3', 'HSD17B6']). Genes only occuring in the table can be introduced by cropping (`SpatialData`), but differences may also be due to the filtering used to generate the transcript data and expression matrix. Please check that `filter_kwargs`, particularly `min_qv` and `control_prefixes`, are consistent with the filtering used to generate the expression table.
  sdata_new = _filter_control_and_low_quality_transcripts(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:190: RuntimeWarning: Cell IDs differ between assigned transcript points and the expression table. 0 cells occur only in points (e.g. []), and 18 occur only in the table (e.g. [np.int32(14112), np.int32(15585), np.int32(14626), np.int32(13762), np.int32(15971)]). Cells only occuring in the table can reflect zero-count cells, but differences may also be due to the filtering used to generate the transcript data and expression matrix. Please check that `filter_kwargs`, particularly `min_qv` and `control_prefixes`, are consistent with the filtering used to generate the expression table.
  sdata_new = _filter_control_and_low_quality_transcripts(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:162: UserWarning: points_background_id '4294967295' is not the most common cell ID among points (most common is '2021'). This may indicate that your background ID is not correctly set. If you are sure that 'points_background_id' is correct, you can ignore this warning.
  validate_spatialdata(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:162: RuntimeWarning: No area column specified for tables. Area will be automatically computed from shapes.
  validate_spatialdata(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/utils.py:1854: UserWarning: Duplicate IDs detected in index 'cell_id' for shapes 'nucleus_boundaries'. Resetting and renaming index to `segtraq_id` to ensure uniqueness.
  nucleus_shapes = _ensure_index(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:190: UserWarning: Filtering control probes matching prefixes ('NegControlProbe_', 'antisense_', 'NegControlCodeword', 'BLANK_', 'Blank-', 'NegPrb', 'DeprecatedCodeword_', 'UnassignedCodeword_', 'Intergenic_Region_') and transcripts with qv < 20 from transcript points in-place; control probes are also removed from the expression table if present. Configure `min_qv`, `control_prefixes`, and `inplace` via `filter_kwargs`.
  sdata_new = _filter_control_and_low_quality_transcripts(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:190: RuntimeWarning: Gene sets differ between assigned transcript points and the expression table. 0 genes occur only in points (e.g. []), and 447 occur only in the table (e.g. ['CACNG5', 'CARD9', 'CDH22', 'VAMP5', 'POU2F3']). Genes only occuring in the table can be introduced by cropping (`SpatialData`), but differences may also be due to the filtering used to generate the transcript data and expression matrix. Please check that `filter_kwargs`, particularly `min_qv` and `control_prefixes`, are consistent with the filtering used to generate the expression table.
  sdata_new = _filter_control_and_low_quality_transcripts(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/SegTraQ.py:190: RuntimeWarning: Cell IDs differ between assigned transcript points and the expression table. 14 cells occur only in points (e.g. [np.int64(12228), np.int64(1094), np.int64(7463), np.int64(426), np.int64(5402)]), and 47 occur only in the table (e.g. [np.int64(16007), np.int64(12296), np.int64(12948), np.int64(7957), np.int64(5272)]). Cells only occuring in the table can reflect zero-count cells, but differences may also be due to the filtering used to generate the transcript data and expression matrix. Please check that `filter_kwargs`, particularly `min_qv` and `control_prefixes`, are consistent with the filtering used to generate the expression table.
  sdata_new = _filter_control_and_low_quality_transcripts(

Load reference scRNA-seq data and transfer labels#

To evaluate how SegTraQ metrics differ between cell types, we first transfer labels from the reference scRNA-seq dataset to the spatial data using segtraq.run_label_transfer.

[7]:
# Load scRNA-seq data
adata_ref = ad.read_h5ad("../../data/xenium_5K_data/BC_scRNAseq_Janesick.h5ad")

For easier access, we store the SpatialData object into a dictionary.

[8]:
st_dict = {"xenium": st_xenium, "bidcell": st_bidcell, "proseg": st_proseg}
[9]:
# Transfer labels
for _method, st in st_dict.items():
    st.run_label_transfer(adata_ref, ref_cell_type="celltype_major", ref_raw_counts_layer="raw", inplace=True)
WARNING: adata.X seems to be already log-transformed.
WARNING: adata.X seems to be already log-transformed.
WARNING: adata.X seems to be already log-transformed.

We can visualize the spatial data and color cell masks by transferred labels using spatialdata-plot.

[10]:
# Define color palette for plotting
col_celltype = {
    "T": "#fb8072",
    "B": "#bc80bd",
    "macro": "#910290",
    "dendritic": "#fdb462",
    "mast": "#959059",
    "perivas": "#fed9a6",
    "endo": "#a6cee3",
    "myoepi": "#2782bb",
    "DCIS1": "#3c7761",
    "DCIS2": "#66a61e",
    "tumor": "#66c2a5",
    "stromal": "#d45943",
    "Unknown": "#808080",
}

Cells with a transcripts count < 10 and a gene count < 5 are not considered in label transfer and their returned transferred_cell_type returns np.nan, and missing values are subsequently replaced with the label “Unknown”.

[11]:
# Replace NaN with Unknown for plotting
for _method, st in st_dict.items():
    s = st.sdata.tables["table"].obs["transferred_cell_type"]
    if pd.api.types.is_categorical_dtype(s):
        s = s.cat.add_categories(["Unknown"])

    st.sdata.tables["table"].obs["transferred_celltype_plot"] = s.fillna("Unknown")
/scratch/jobs/61866415/ipykernel_1844593/1864087363.py:4: DeprecationWarning: is_categorical_dtype is deprecated and will be removed in a future version. Use isinstance(dtype, pd.CategoricalDtype) instead
  if pd.api.types.is_categorical_dtype(s):
/scratch/jobs/61866415/ipykernel_1844593/1864087363.py:4: DeprecationWarning: is_categorical_dtype is deprecated and will be removed in a future version. Use isinstance(dtype, pd.CategoricalDtype) instead
  if pd.api.types.is_categorical_dtype(s):
/scratch/jobs/61866415/ipykernel_1844593/1864087363.py:4: DeprecationWarning: is_categorical_dtype is deprecated and will be removed in a future version. Use isinstance(dtype, pd.CategoricalDtype) instead
  if pd.api.types.is_categorical_dtype(s):
[12]:
plt.style.use("dark_background")
[13]:
def feature_spatial_plot(sdata, shapes_key, feature, col_palette, method):
    axes = plt.subplots(1, 2, figsize=(10, 5), constrained_layout=True)[1].flatten()

    # compute per-cell color
    labels = sdata.tables["table"].obs[feature].unique().astype(str).tolist()
    cols = [col_palette[lab] for lab in labels]

    # Dapi image
    sdata.pl.render_images("image").pl.show(ax=axes[0], title=f"{method}: DAPI image", coordinate_systems="global")

    # Plot - Cell boundaries
    sdata.tables["table"].obs["region"] = shapes_key
    sdata.set_table_annotates_spatialelement("table", region=shapes_key)

    sdata.pl.render_shapes(shapes_key, color=feature, palette=cols, groups=labels).pl.show(
        ax=axes[1], title=f"{method}: Cell masks colored by {feature}", coordinate_systems="global"
    )

10 Xenium MultiModal Cell Segmentation

[14]:
feature_spatial_plot(st_xenium.sdata, "cell_boundaries", "transferred_celltype_plot", col_celltype, "xenium")
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/.venv/lib/python3.13/site-packages/spatialdata/_core/spatialdata.py:477: UserWarning: Converting `region_key: region` to categorical dtype.
  convert_region_column_to_categorical(table)
../_images/notebooks_10x_xenium_focus_24_1.png

BIDCell Segmentation

[15]:
feature_spatial_plot(st_bidcell.sdata, "cell_boundaries", "transferred_celltype_plot", col_celltype, "BIDCell")
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/.venv/lib/python3.13/site-packages/spatialdata/_core/spatialdata.py:477: UserWarning: Converting `region_key: region` to categorical dtype.
  convert_region_column_to_categorical(table)
../_images/notebooks_10x_xenium_focus_26_1.png

Proseg Segmentation

[16]:
feature_spatial_plot(st_proseg.sdata, "cell_boundaries_z1", "transferred_celltype_plot", col_celltype, "Proseg")
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/.venv/lib/python3.13/site-packages/spatialdata/_core/spatialdata.py:477: UserWarning: Converting `region_key: region` to categorical dtype.
  convert_region_column_to_categorical(table)
../_images/notebooks_10x_xenium_focus_28_1.png

The dominant cell types are DCIS2, T and B cells. Proseg has the highest proportion of cells assigned to “Unknown”, i.e. transcript counts <10 and gene_counts <5.

[17]:
method_dfs = []
for method, st in st_dict.items():
    df = st.sdata["table"].obs["transferred_celltype_plot"].value_counts(normalize=True).reset_index()
    df.columns = ["celltype", "proportion"]
    df["method"] = method
    method_dfs.append(df)

plot_df = pd.concat(method_dfs, ignore_index=True)
plot_wide = plot_df.pivot(index="method", columns="celltype", values="proportion").fillna(0)

method_order = ["xenium", "bidcell", "proseg"]
plot_wide = plot_wide.loc[method_order]

fig, ax = plt.subplots(figsize=(2, 4))

plot_wide.plot(kind="bar", stacked=True, color=[col_celltype.get(c, "grey") for c in plot_wide.columns], ax=ax)

ax.set_ylabel("Proportion")
ax.set_xlabel("Method")

# Legend text in white
legend = ax.legend(title="Cell type", bbox_to_anchor=(1.05, 1), loc="upper left", frameon=False)

fig.tight_layout()
plt.show()
/scratch/jobs/61866415/ipykernel_1844593/990135166.py:24: UserWarning: Tight layout not applied. The left and right margins cannot be made large enough to accommodate all Axes decorations.
  fig.tight_layout()
../_images/notebooks_10x_xenium_focus_30_1.png

Running SegTraQ QC metrics#

Now the data is ready to compute SegTraQ QC metrics. Below we demonstrate how to run the individual modules.

Baseline module#

The baseline (bl) module computes basic quality-control metrics such as the number of cells, the percentage of unassigned transcripts, and the number of transcripts and genes per cell. When inplace=True, global metrics are stored in the uns slot, and per-cell metrics are stored in the obs slot of the table within the SpatialData object.

[18]:
for method, st in st_dict.items():
    st.bl.num_cells(inplace=True)
    st.bl.perc_unassigned_transcripts(inplace=True)
    st.bl.transcripts_per_cell()
    st.bl.genes_per_cell()
    st.bl.mean_transcripts_per_gene_per_cell()

    num_cells = st.sdata.tables["table"].uns["num_cells"]
    p_unassigned = st.sdata.tables["table"].uns["perc_unassigned_transcripts"]
    mean_tx = st.sdata.tables["table"].obs["transcript_count"].mean()
    mean_gn = st.sdata.tables["table"].obs["gene_count"].mean()
    mean_tgc = st.sdata.tables["table"].obs["mean_transcripts_per_gene"].mean()

    print(f"\n{method}")
    print(f"#cells: {num_cells}; %unassigned: {p_unassigned}")
    print(f"mean #transcripts: {mean_tx}; mean #genes: {np.mean(mean_gn)}")
    print(f"mean #transcripts per gene per cell: {mean_tgc}")

xenium
#cells: 751; %unassigned: 7.701023026425406
mean #transcripts: 110.6444740346205; mean #genes: 91.77762982689747
mean #transcripts per gene per cell: 1.129141779055404

bidcell
#cells: 766; %unassigned: 7.9956894157380765
mean #transcripts: 108.11227154046998; mean #genes: 89.4425587467363
mean #transcripts per gene per cell: 1.0992300606892813

proseg
#cells: 756; %unassigned: 0.8153109622668755
mean #transcripts: 118.04497354497354; mean #genes: 93.23941798941799
mean #transcripts per gene per cell: 1.1219463970863126

The number of detected cells is comparable across Xenium, BIDCell, and Proseg. Proseg leaves the smallest fraction of transcripts unassigned (0.81%). The number of transcripts and genes per cell can be visualized per cell type, as shown below.

[19]:
# method for visualization
def boxplot_per_celltype(st_dict, feature, q=1):
    dfs = []
    for method, st in st_dict.items():
        obs = st.sdata["table"].obs[st.sdata["table"].obs["transferred_cell_type"].notna()]
        tmp = obs[["transferred_cell_type", feature]].copy()
        tmp["method"] = method
        dfs.append(tmp)

    df = pd.concat(dfs, ignore_index=True)
    df = df[(df[feature] <= df[feature].quantile(q))]

    fig, ax = plt.subplots(figsize=(15, 5))

    sns.boxplot(
        data=df,
        x="transferred_cell_type",
        y=feature,
        hue="method",
        showcaps=True,
        showfliers=False,
        palette="Set2",
        ax=ax,
    )

    fig.tight_layout()
    plt.show()

Proseg yields the highest mean number of transcripts per cell, driven by increased transcript detection in DCIS2 and T, B and stromal cells, which constitute the dominant cell populations in the dataset. In contrast, Proseg detects fewer transcripts in some of the less abundant cell types, such as DCIS1 and myoepithelial cells.

[20]:
boxplot_per_celltype(st_dict, "transcript_count")
../_images/notebooks_10x_xenium_focus_39_0.png

The number of genes detected per cell correlates strongly with the number of transcripts per cell.

[21]:
boxplot_per_celltype(st_dict, "gene_count")
../_images/notebooks_10x_xenium_focus_41_0.png

On average, the number of transcripts per gene per cell is approximately 1, indicating that across methods we detect roughly one to two transcripts per gene per cell. Among the methods, Proseg shows the highest mean number of transcripts per gene per cell when the Unknown cluster (cells with transcript counts < 10 and gene counts < 5) is excluded.

Interestingly, Proseg detects more transcripts per gene per cell across all cell types, even in cases where the total transcript and gene counts per cell are lower, e.g., DCIS1 and myoepithelial cells. This pattern suggests that transcripts are concentrated among fewer genes per cell, potentially reflecting more homogeneous within-cell expression profiles (i.e. reduced gene-level variability).

[22]:
boxplot_per_celltype(st_dict, "mean_transcripts_per_gene")
../_images/notebooks_10x_xenium_focus_43_0.png

The baseline module also facilitates the computation of morphological features.

[23]:
for _method, st in st_dict.items():
    st.bl.morphological_features(n_jobs=8)
[24]:
features = [
    "cell_area",
    "perimeter",
    "circularity",
    "solidity",
    "convexity",
    "elongation",
    "eccentricity",
    "compactness",
]

# Collect features into one dataframe
all_feats = []
for method, st in st_dict.items():
    obs = st.sdata["table"].obs[st.sdata["table"].obs["transferred_cell_type"].notna()]
    feat = obs[features]
    tmp = feat.copy()
    tmp["method"] = method
    all_feats.append(tmp)

df = pd.concat(all_feats, ignore_index=True)
df["method"] = df["method"].astype(str)

feature_cols = [c for c in df.columns if c != "method"]

fig, ax = plt.subplots(2, 6, figsize=(6 * 4, 2 * 3))
ax = ax.flatten()

for i, feat_name in enumerate(feature_cols):
    sns.kdeplot(data=df, x=feat_name, hue="method", palette="Set2", common_norm=False, fill=False, ax=ax[i])
    ax[i].set_title(f"Distribution of {feat_name}")
    ax[i].set_xlabel(feat_name)
    ax[i].set_ylabel("Density")
fig.tight_layout()
plt.show()
../_images/notebooks_10x_xenium_focus_46_0.png

Xenium detects rounder cells, characterized by higher circularity and solidity, while BIDCell recovers a wider distribution of cell shapes, including both low and high circularity and solidity. BIDCell incorporates a loss term enforcing cell type–specific elongation, which may explain the detection of less circular cell shapes. Notably, the same cell types specified as elongated during training (perivascular, myoepithelial, stromal, and endothelial cells) also exhibit the highest median elongation in the results, highlighting the strong influence of the prior on the inferred cell shapes.

[25]:
obs = st_bidcell.sdata["table"].obs[st_bidcell.sdata["table"].obs["transferred_cell_type"].notna()]
df = obs[["transferred_cell_type", "elongation"]]
medians = df.groupby("transferred_cell_type").median()
order = medians.sort_values(by="elongation").index

fig, ax = plt.subplots(figsize=(10, 3))

sns.boxplot(
    data=df,
    x="transferred_cell_type",
    y="elongation",
    order=order,
    showcaps=True,
    showfliers=False,
    palette=col_celltype,
    ax=ax,
)

ax.set_title("BIDCell - cell shape representation")
fig.tight_layout()
plt.show()
/scratch/jobs/61866415/ipykernel_1844593/3127299547.py:3: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
  medians = df.groupby("transferred_cell_type").median()
/scratch/jobs/61866415/ipykernel_1844593/3127299547.py:8: FutureWarning:

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.boxplot(
../_images/notebooks_10x_xenium_focus_48_1.png

Proseg, in contrast, does not rely solely on membrane stainings. Instead, it incorporates transcript positions, which may extend beyond the membrane boundary due to diffusion. This may explain why perivascular or myoepithelial cells do not appear among the most elongated cell types. Importantly, a segmentation mask that accurately delineates cell geometry may still misassign transcripts (e.g. due to diffusion or spatial overlap), while a mask that poorly represents cell shape may nonetheless correctly assign transcripts. Therefore, it is essential to evaluate not only the geometric accuracy of cell shapes but also the accuracy of transcript assignment to cells. We will compute metrics to evaluate transcript assignment accuracy below.

[26]:
obs = st_proseg.sdata["table"].obs[st_proseg.sdata["table"].obs["transferred_cell_type"].notna()]
df = obs[["transferred_cell_type", "elongation"]]
medians = df.groupby("transferred_cell_type").median()
order = medians.sort_values(by="elongation").index

fig, ax = plt.subplots(figsize=(10, 3))

sns.boxplot(
    data=df,
    x="transferred_cell_type",
    y="elongation",
    order=order,
    showcaps=True,
    showfliers=False,
    palette=col_celltype,
    ax=ax,
)

ax.set_title("Proseg - cell shape representation")
fig.tight_layout()
plt.show()
/scratch/jobs/61866415/ipykernel_1844593/2145051797.py:3: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
  medians = df.groupby("transferred_cell_type").median()
/scratch/jobs/61866415/ipykernel_1844593/2145051797.py:8: FutureWarning:

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.boxplot(
../_images/notebooks_10x_xenium_focus_50_1.png

Clustering stability module#

The clustering stability (cs) module provides metrics for assessing the stability of clustering results across different resolutions and random subsets of genes.

Let`s first perform Leiden clustering and visualize the clusters in the UMAP space.

[27]:
fig, axs = plt.subplots(1, 3, figsize=(15, 5))

axs = axs.flatten()

for i, (method, st) in enumerate(st_dict.items()):
    adata = st.sdata.tables["table"].copy()
    adata.layers["counts"] = adata.X
    sc.pp.normalize_total(adata, inplace=True)
    sc.pp.log1p(adata)
    sc.pp.pca(adata)
    sc.pp.neighbors(adata)
    sc.tl.umap(adata)
    sc.tl.leiden(adata, flavor="igraph", n_iterations=2)

    sc.pl.umap(
        adata,
        color="leiden",
        palette="Set2",
        ax=axs[i],
        show=False,
        title=method,
    )

plt.tight_layout()
plt.show()
/scratch/jobs/61866415/ipykernel_1844593/1436914766.py:8: UserWarning: Some cells have zero counts
  sc.pp.normalize_total(adata, inplace=True)
/scratch/jobs/61866415/ipykernel_1844593/1436914766.py:8: UserWarning: Some cells have zero counts
  sc.pp.normalize_total(adata, inplace=True)
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/.venv/lib/python3.13/site-packages/scipy/sparse/_index.py:210: SparseEfficiencyWarning: Changing the sparsity structure of a csr_matrix is expensive. lil and dok are more efficient.
  self._set_arrayXarray(i, j, x)
../_images/notebooks_10x_xenium_focus_53_1.png

Next, we compute clustering stability metrics and plot these.

[28]:
ccds = {}
silhouette_scores = {}
purities = {}
aris = {}

for method, st in st_dict.items():
    ccds[method] = st.cs.cluster_connectedness()
    silhouette_scores[method] = st.cs.silhouette_score()
    purities[method] = st.cs.purity()
    aris[method] = st.cs.adjusted_rand_index()
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/src/segtraq/cs/utils.py:218: FutureWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
In the future, the default backend for leiden will be igraph instead of leidenalg. To achieve the future defaults please pass: `flavor='igraph'` and `n_iterations=2`. `directed` must also be `False` to work with igraph’s implementation.
  sc.tl.leiden(
[29]:
results_df = pd.DataFrame(
    {
        "Method": list(ccds.keys()),
        "Connectedness": list(ccds.values()),
        "Silhouette Score": list(silhouette_scores.values()),
        "Purity": list(purities.values()),
        "ARI": list(aris.values()),
    }
)
[30]:
fig, ax = plt.subplots(figsize=(4, 3))

sns.scatterplot(
    data=results_df,
    x="Connectedness",
    y="Silhouette Score",
    hue="Method",
    style="Method",
    s=100,
    palette="Set2",
    ax=ax,
)

ax.set_xlabel("Connectedness (↑)")
ax.set_ylabel("Silhouette Score (↑)")
ax.set_title("Connectedness vs. Silhouette Score")
ax.set_xlim(0, 1)
ax.set_ylim(-1, 1)
legend = ax.legend(title="Method", frameon=False, bbox_to_anchor=(1.05, 1), loc="upper left")

fig.tight_layout()
plt.show()
../_images/notebooks_10x_xenium_focus_57_0.png
[31]:
fig, ax = plt.subplots(figsize=(4, 3))

sns.scatterplot(
    data=results_df,
    x="Purity",
    y="ARI",
    hue="Method",
    style="Method",
    s=100,
    palette="Set2",
    ax=ax,
)

ax.set_xlabel("Purity (↑)")
ax.set_ylabel("ARI (↑)")
ax.set_title("Purity vs. ARI")
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)

legend = ax.legend(title="Method", frameon=False, bbox_to_anchor=(1.05, 1), loc="upper left")

fig.tight_layout()
plt.show()
../_images/notebooks_10x_xenium_focus_58_0.png

Proseg achieves the highest adjusted Rand index (ARI) and clustering purity, as well as the lowest mean cosine distance. Together, these results suggest that Proseg retrieves the most internally consistent expression profiles and could indicate less contaminated single cell signatures.

The silhouette score can also be computed on the transferred labels. We will first visualize transferred labels in the UMAP space.

[32]:
fig, axs = plt.subplots(1, 3, figsize=(15, 5))

axs = axs.flatten()

for i, (method, st) in enumerate(st_dict.items()):
    adata = st.sdata.tables["table"].copy()

    # normalizing and log-transforming the counts
    sc.pp.normalize_total(adata, inplace=True)
    sc.pp.log1p(adata)
    # computing a PCA and neighbors
    sc.pp.pca(adata)
    sc.pp.neighbors(adata)
    sc.tl.umap(adata)

    sc.pl.umap(
        adata,
        color="transferred_cell_type",
        palette=col_celltype,
        ax=axs[i],
        show=False,
        title=method,
    )

plt.tight_layout()
plt.show()
/scratch/jobs/61866415/ipykernel_1844593/3004694059.py:9: UserWarning: Some cells have zero counts
  sc.pp.normalize_total(adata, inplace=True)
WARNING: adata.X seems to be already log-transformed.
/scratch/jobs/61866415/ipykernel_1844593/3004694059.py:9: UserWarning: Some cells have zero counts
  sc.pp.normalize_total(adata, inplace=True)
/g/huber/users/meyerben/notebooks/spatial_transcriptomics/SegTraQ/.venv/lib/python3.13/site-packages/scipy/sparse/_index.py:210: SparseEfficiencyWarning: Changing the sparsity structure of a csr_matrix is expensive. lil and dok are more efficient.
  self._set_arrayXarray(i, j, x)
../_images/notebooks_10x_xenium_focus_60_3.png

Proseg achieves the highest silhouette score for the transferred labels, indicating higher intra-cluster coherence and improved inter-cluster separation; this is also visually consistent with the UMAP embedding.

[33]:
silhouette_scores_labels = {}

for method, st in st_dict.items():
    silhouette_scores_labels[method] = st.cs.silhouette_score(cell_type_key="transferred_cell_type")

silhouette_scores_labels
[33]:
{'xenium': -0.04281586408615112,
 'bidcell': -0.026861874386668205,
 'proseg': 0.02360549382865429}

Region similarity module#

While individual genes may exhibit subcellular localization patterns, the overall distribution of transcripts, when averaged across genes, is expected to be relatively smooth and approximately uniform within a cell. Based on this assumption, the region similarity module evaluates the similarity of gene expression profiles across different subcellular compartments. Deviations from this expected intra-cellular consistency can serve as indicators of transcript contamination originating from neighboring cells.

Intersection over Union (IoU)

Proseg shows the lowest intersection over union (IoU) between the cell and nucleus. It relies on transcript assignment rather than explicit morphology-based segmentation, allowing transcripts that are repositioned outside the cell membrane (e.g. due to transcript diffusion) to be reassigned to the cell of origin. This often results in larger inferred cell areas (or perimeters, as shown above) relative to the nucleus or less aligned cell and nucleus shapes, leading to lower cell–nucleus IoU values.

[34]:
plt.style.use("default")


def plot_IoU(sdata, shapes_key, method, ax):
    # note that copy.deepcopy() can have unwanted effects on the attrs, hence sd.deepcopy() is preferred
    sdata_plot = sd.deepcopy(sdata)
    sdata_plot["table"] = sdata_plot["table"][(sdata_plot["table"].obs["iou"].notna())]

    sdata_plot.tables["table"].obs["region"] = shapes_key
    sdata_plot.set_table_annotates_spatialelement("table", region=shapes_key)

    sdata_plot.pl.render_shapes(
        element="nucleus_boundaries",
        fill_alpha=0.2,
        outline_alpha=1.0,
        outline_width=0.5,
        outline_color="black",
    ).pl.render_shapes(
        element=shapes_key,
        color="iou",
        cmap="viridis",
        fill_alpha=0.5,
        outline_alpha=1.0,
        outline_width=0.5,
        outline_color="black",
    ).pl.show(ax=ax, title=f"{method}: Overlay of nuclei and cell masks colored by IoU", colorbar=True)
[35]:
fig, axes = plt.subplots(1, 3, figsize=(15, 5), constrained_layout=True)

for i, (method, st) in enumerate(st_dict.items()):
    st.rs.match_nuclei_to_cells(n_jobs=-1)
    plot_IoU(st.sdata, "cell_boundaries", method, axes[i])
../_images/notebooks_10x_xenium_focus_67_0.png

Similarity between expression in cell and nucleus

Once, the “best-matching” nucleus is determined for each cell based on IoU, we can compute the similarity between cell and nuclear expression.

[36]:
for _method, st in st_dict.items():
    st.rs.similarity_nucleus_cell(n_jobs=-1)
[37]:
def histogram_feature(
    sdata_dict,
    feature,
    figsize=(8, 4),
    significant_only=True,
    bins=30,
):
    plt.style.use("default")
    all_feats = []

    for method, st in sdata_dict.items():
        obs = st.sdata[st.tables_key].obs

        if significant_only:
            p_value_col = f"{feature}_p_value"
            if feature == "border_admixture_score":
                p_value_col = "border_admixture_p_value"
            obs = obs.loc[obs[p_value_col] < 0.05]

        feat = obs[feature].to_frame(feature)
        feat["method"] = method
        all_feats.append(feat)

    df = pd.concat(all_feats, ignore_index=True)
    df["method"] = df["method"].astype(str)

    methods = list(sdata_dict.keys())
    palette = dict(zip(methods, sns.color_palette("Set2", len(methods)), strict=False))

    fig, ax = plt.subplots(figsize=figsize)

    sns.histplot(
        data=df,
        x=feature,
        hue="method",
        bins=bins,
        stat="count",
        element="step",
        # edgecolor=None,
        palette=palette,
        ax=ax,
        alpha=0.3,
        line_kws=dict(color="method", linewidth=2.5, label="KDE"),
        kde=True,
    )

    # Median for each method
    for method in methods:
        median = df.loc[df["method"] == method, feature].median()

        ax.axvline(
            median,
            color=palette[method],
            linestyle="--",
            linewidth=2,
        )

    ax.set_ylabel("Number of cells")
    plt.tight_layout()

Among cells with unexpectedly low nucleus–cell similarity (similarity_nucleus_cell_p_value < 0.05), ProSeg shows more negative residuals (similarity_nucleus_cell) than Xenium and BIDCell, indicating a stronger difference between the nuclear and whole-cell expression profiles than expected. This is partly expected from the different assignment strategies. Xenium and BIDCell assign transcripts to cells based on spatial overlap with the inferred cell boundaries, whereas ProSeg can assign transcripts to a cell even when they lie outside its final 2D cell boundary. Because the nuclear expression profile is also defined by spatial overlap with the nuclear boundary, the nucleus and whole-cell profiles are more spatially coupled for Xenium and BIDCell. Accordingly, among these cells, Xenium and BIDCell show less negative residuals than ProSeg. Thus, ProSeg’s lower nucleus–cell similarity may partly reflect its transcript reassignment strategy rather than poorer segmentation.

[38]:
histogram_feature(st_dict, "similarity_nucleus_cell", significant_only=True)
../_images/notebooks_10x_xenium_focus_72_0.png

While similarity_nucleus_cell compares the nuclear transcript profile with the final whole-cell expression profile, similarity_nucleus_cytoplasm more directly compares two spatial regions within the cell. It compares the cell’s transcripts regionally overlapping with the nucleus and the non-nuclear region (cytoplasm). It therefore asks whether the transcript composition is consistent across these two regions of the same cell. Here, Proseg again shows the lowest consistency between nuclear and cytoplasmic expression profiles.

[39]:
for _method, st in st_dict.items():
    st.rs.similarity_nucleus_cytoplasm(n_jobs=-1)
[40]:
histogram_feature(st_dict, "similarity_nucleus_cytoplasm")
../_images/notebooks_10x_xenium_focus_75_0.png

Admixture (contamination) of the cell border

Unlike the nucleus-based similarity metrics, the border_admixture_score more directly tests for contamination from neighboring cells. Rather than asking whether two regions within a cell have different expression profiles, it asks whether the cell border specifically resembles a mixture of the cell center and the surrounding neighborhood more strongly than expected from finite transcript sampling alone. This helps distinguish general within-cell expression differences from differences that specifically point toward a contribution from neighboring cells.

Here, Xenium has the largest number of cells with unexpectedly strong border admixture (border_admixture_p_value < 0.05), meaning that for more Xenium cells, the border resembles the surrounding neighborhood more than would be expected by chance. Xenium also shows the largest median positive residual among these cells, indicating that the excess neighborhood-like signal at the border tends to be stronger. In contrast, BIDCell and Proseg — both transcript-informed segmentation methods — show substantially fewer cells with evidence of unexpected border admixture, and the effect is generally weaker when it occurs.

[41]:
for _method, st in st_dict.items():
    st.rs.border_admixture_score(n_jobs=-1)
[42]:
histogram_feature(st_dict, "border_admixture_score")
../_images/notebooks_10x_xenium_focus_78_0.png

While it is important to consider metrics that do not depend on the availability of high-quality reference scRNA-seq data—and therefore do not rely on the performance of label transfer—a limitation of the region similarity module is that it cannot distinguish between homogeneous and heterogeneous cellular neighborhoods. In homogeneous neighborhoods, a high border_admixture_score does not necessarily indicate contamination. Nevertheless, when comparing methods globally or on a cell-type level, this is a useful unsupervised contamination metric that does not rely on reference scRNA-seq data.

When cell type labels are available, the metric can still be compared on a cell-type level. The plot compares border_admixture_score across segmentation methods within each transferred cell type, restricted to cells with unexpectedly strong border admixture (border_admixture_p_value < 0.05). The individual points and boxplots show the magnitude of the excess neighborhood-like signal at the border, while the annotated cell counts show how many cells of each cell type are significantly affected. This helps reveal whether differences between segmentation methods are driven by particular cell types rather than representing a global effect.

[43]:
def boxplot_per_celltype_sig(st_dict, feature, q=1, significant_only=True):
    dfs = []
    for method, st in st_dict.items():
        obs = st.sdata[st.tables_key].obs[st.sdata[st.tables_key].obs["transferred_cell_type"].notna()].copy()

        if (feature == "similarity_top_bottom") and significant_only:
            obs = obs.loc[obs["similarity_top_bottom_p_value"] < 0.05].copy()

        obs["transferred_cell_type"] = obs["transferred_cell_type"].cat.remove_unused_categories()

        tmp = obs[["transferred_cell_type", feature]].copy()
        tmp["method"] = method
        dfs.append(tmp)

    df = pd.concat(dfs, ignore_index=True)
    df = df[df[feature] <= df[feature].quantile(q)]

    fig, ax = plt.subplots(figsize=(15, 5))

    sns.boxplot(
        data=df,
        x="transferred_cell_type",
        y=feature,
        hue="method",
        showcaps=True,
        showfliers=False,
        palette="Set2",
        boxprops={"alpha": 0.5},
        ax=ax,
    )

    sns.stripplot(
        data=df,
        x="transferred_cell_type",
        y=feature,
        hue="method",
        dodge=True,
        jitter=True,
        palette="Set2",
        alpha=1,
        size=3,
        legend=False,
        ax=ax,
    )

    celltypes = df["transferred_cell_type"].unique()
    methods = list(st_dict.keys())

    n_methods = len(methods)
    box_width = 0.8
    offsets = np.linspace(
        -box_width / 2 + box_width / (2 * n_methods),
        box_width / 2 - box_width / (2 * n_methods),
        n_methods,
    )

    # Position labels slightly above the data
    ymin, ymax = ax.get_ylim()
    y_text = ymax + 0.02 * (ymax - ymin)

    for i, celltype in enumerate(celltypes):
        for j, method in enumerate(methods):
            n = len(df.loc[(df["transferred_cell_type"] == celltype) & (df["method"] == method)])

            ax.text(
                i + offsets[j],
                y_text,
                f"n={n}",
                ha="center",
                va="bottom",
                fontsize=8,
                rotation=90,
            )

    # Make room for annotations
    ax.set_ylim(ymin, ymax + 0.12 * (ymax - ymin))

    fig.tight_layout()
    plt.show()
[44]:
boxplot_per_celltype_sig(st_dict, "border_admixture_score")
../_images/notebooks_10x_xenium_focus_82_0.png

Supervised module#

The sp (supervised) module provides metrics to evaluate how well cell profiles in a spatial transcriptomics dataset agree with a reference single-cell RNA-seq (scRNA-seq) dataset with cell type annotations.

Unlike scRNA-seq, contamination in spatial transcriptomics measurements mostly originates from the local tissue context.

By comparing spatial expression profiles to a high-quality scRNA-seq reference, the supervised module aims to quantify this mismatch. Specifically, we compute metrics that measure:

  • how well each spatial cell matches its expected cell type,

  • how much its expression resembles other (neighboring) cell types, and

  • if it is possible to predict that a cell of one cell type is adjacent to a different cell type.

To obtain cell-type specific marker genes, we define positive and negative markers in the annotated scRNA-seq via markers_from_reference.

Computing cell-type specific markers

[45]:
markers = {}
for method, st in st_dict.items():
    markers[method] = st.markers_from_reference(
        adata_ref,
        ref_cell_type="celltype_major",
        ref_raw_counts_layer="raw",
        n_jobs=16,
    )
WARNING: adata.X seems to be already log-transformed.
WARNING: adata.X seems to be already log-transformed.
WARNING: adata.X seems to be already log-transformed.

A list of positive and negative markers computed for each cell type is shown below. We will use xenium for demonstration.

[46]:
for cell_type, sign in markers["xenium"].items():
    pos = sign["positive"]
    neg = sign["negative"]

    print(f"{cell_type}  |  positive: {', '.join(pos)}")
    print(f"{cell_type}  |  negative: {', '.join(neg)}\n")
B  |  positive: PAX5, MS4A1, BANK1, FCRL1, BLK, FCRL2, AIM2, TNFRSF13B, CD19, POU2AF1, SCIMP, FCRL5, P2RX5, CD79A, PARP15, CD79B, TNFRSF13C, POU2F2, FCRLA, STAP1, CD22, COL4A3, SP140, NIBAN3, CR1, ADAM28, PLCG2, FCMR, CD40, TLR6, ST6GAL1, DERL3, KCNN4, CD27, CD52, ITGB7, BLNK, CNR2, COL4A4, SPIB, IRF4, TNFRSF17, ZBP1, PTK2B, FCGR2B, CDK14, PTPN6, TNFAIP8, SP110, ATM, FCRL3, COCH, CCDC141, NCF1, CD38, CCR6, ITGAL, RELT, PIM2, SMCHD1, ANKRD13A, NCOA3, PDK1, LRRK2, KCNA3, NLRP1, TLR1, SLAMF1, SLAMF6, GPR174, JAK3, GRK5, NLRC5, SMAP2, BCL2, SYVN1, TENT5C, ORAI2, CASP10, CYSLTR1, CD83, LPXN, TRAF5, REL, BTN3A1, SYNRG, ATP2B1, SEMA4B
B  |  negative: ABCA10, ABCA8, ABCC8, ABCC9, ABI3BP, AC007906.2, ACVRL1, ADAM12, ADAM33, ADAMDEC1, ADAMTS1, ADAMTS12, ADAMTS14, ADAMTS2, ADAMTS4, ADAMTS5, ADCYAP1, ADGRE2, ADGRL4, ADORA2B, ADORA3, ADRA1B, ADRA2A, ADTRP, AFAP1L2, AGR3, AIF1, ALDH7A1, ALPK3, ALPL, ANGPT1, ANGPTL2, ANGPTL4, ANXA3, ANXA8, APLNR, AQP1, ASPN, ATL1, AVPR1A, AXIN2, BAALC, BARX2, BCL6B, BDKRB2, BMP2, BMPR1B, BNC2, BPIFB1, BRINP1, C1QTNF1, C3AR1, C4B, C5AR1, C7, CACNA1B, CACNG4, CADM3, CALB2, CALCRL, CALML3, CARD10, CARD14, CARD9, CAVIN2, CAVIN3, CCDC102B, CCL14, CCL19, CCL22, CCL28, CCN3, CCN4, CCR4, CCR5, CD1E, CD209, CD248, CD274, CD302, CD33, CD34, CD40LG, CD7, CD8A, CDH13, CDH23, CDH3, CDH5, CDH6, CDKN2B, CEBPA, CELSR3, CENPA, CEP112, CES1, CFC1, CGNL1, CHAD, CHST8, CLCN5, CLDN5, CLEC10A, CLEC12A, CLEC14A, CLEC4C, CLEC5A, CLEC7A, CLGN, CLIC3, CMA1, CMKLR1, CMYA5, CNN1, CNRIP1, CNTNAP1, COL10A1, COL17A1, COL7A1, COLEC12, CPVL, CPXM2, CRB2, CSF1, CSF2RA, CSF3R, CSPG4, CST7, CTLA4, CTSG, CTSW, CUX2, CX3CL1, CXCL2, CXCR6, CYP27A1, DCLK1, DDR2, DKK3, DLK1, DLL4, DLX4, DMD, DNAH7, DNASE1L3, DNM1, DPT, DPY19L2, DSC3, DSG3, EBF2, ECRG4, EDN1, EDNRA, EGF, EGFR, ELN, EMP1, ENPP1, ENPP2, ENPP3, ENTPD2, EPB41L3, EPHA2, EPHB1, EREG, ERG, ERRFI1, ESM1, ETV1, F13A1, F2R, F2RL3, F3, F7, FAM107A, FAP, FAT2, FAT4, FBLN2, FBLN5, FCER1A, FCGR3A, FES, FGF1, FGF2, FGF7, FGFR2, FILIP1L, FLRT3, FLT1, FLT3, FLT4, FMN1, FMOD, FOLH1, FOLR2, FOXC2, FOXD2, FPR1, FREM1, FRZB, FSCN1, FST, FZD4, GABRD, GABRP, GHR, GIMAP5, GIMAP8, GJC1, GLI2, GLT8D2, GPR39, GPR4, GPRC5B, GRAMD2B, GRIK3, GRIK4, GRP, GUCY1A2, GZMA, GZMB, GZMK, HAS2, HAVCR2, HBEGF, HCK, HDC, HEYL, HGF, HLX, HMCN1, HOXA5, HOXD10, HOXD9, HPGD, HPGDS, HPSE, HPSE2, HS3ST3A1, HS6ST2, ICOS, IDO1, IGF1, IL17RB, IL18, IL18R1, IL1R1, IL1RL1, IL2RB, IL33, IL34, IL3RA, IL6R, INHBA, IRS1, IRS2, IRX1, ITGA2, ITGA2B, ITGA5, ITGA7, ITIH5, JAG1, JAM2, KANK2, KCNF1, KCNJ8, KCNK10, KCNK15, KCNK17, KCNMA1, KCNMB1, KDR, KIT, KLHL13, KLK10, KLK6, KLRD1, KLRG1, LAMB3, LAMC2, LAMP3, LCN2, LCNL1, LDB3, LEF1, LGR4, LIF, LILRA4, LILRB2, LILRB4, LILRB5, LOX, LOXL2, LPAR1, LPL, LRG1, LRRC15, LTK, LURAP1, MAOB, MAP1A, MAP1B, MAP2, MAP3K7CL, MAPT, ME1, MECOM, MEIS2, MERTK, MET, MFAP5, MLXIPL, MME, MMP11, MMP9, MMRN2, MNDA, MPP1, MRC1, MS4A2, MSR1, MTNR1B, MUC16, MUC5B, MYCL, MYH11, MYLK, NAV2, NCCRP1, NCF2, NGFR, NKX3-1, NLRP2, NLRP3, NOS1AP, NOS3, NOSTRIN, NPNT, NPR1, NPY1R, NR4A3, NR5A2, NRN1, NRP2, NTRK2, OLFM1, OLFML3, OLR1, OPRD1, OXTR, P2RX7, P2RY13, P2RY14, P2RY2, P2RY6, PALMD, PAMR1, PBK, PCDH10, PCDH17, PCDH18, PDE10A, PDE2A, PDE3A, PDE5A, PDGFA, PDGFD, PDGFRA, PDGFRB, PDGFRL, PDLIM4, PDPN, PDZK1, PGF, PGR, PHEX, PHF21B, PHLDB2, PI16, PIEZO2, PILRA, PITX1, PLA2G7, PLAGL1, PLAUR, PLCE1, PLEKHH2, PLVAP, PLXDC1, PMAIP1, PODXL, POTEI, POTEJ, PPARG, PPP1R1C, PRDM6, PRF1, PRKAA2, PRKCQ, PROCR, PROM1, PROX1, PRRX1, PTAFR, PTGDR, PTGES, PTGFRN, PTGIR, PTGIS, PTGS1, PTHLH, PTK7, PTPRB, PTPRM, RAPGEF3, RASD1, RASSF4, RBMS3, RBPMS, RCAN2, RELN, RERG, RGS5, RIMS3, RIPK4, RNF180, RNF39, ROBO4, ROR2, RTN1, RUNX1T1, S100A1, SAMD5, SASH1, SCGB1D2, SCN9A, SDC2, SELP, SEPTIN4, SEPTIN5, SERPINA5, SERPINB5, SERPINE1, SFRP1, SH2D1A, SH2D2A, SH3RF2, SHANK3, SHD, SIGLEC1, SIGLEC9, SIM2, SIRPA, SLAMF8, SLC11A1, SLC18A2, SLC1A3, SLC2A9, SLC35G2, SLC6A1, SLC7A11, SLCO2A1, SLCO2B1, SLIT2, SLIT3, SMOC2, SMPD3, SNCA, SOD3, SOSTDC1, SOX10, SOX17, SOX18, SOX9, SPA17, SPAG17, SPATA18, SPOCK1, SPON1, SPON2, SPRY2, SRPX, STEAP1, STEAP4, SUGCT, SULF1, SVEP1, SYNPO2, TAL1, TBX2, TBXA2R, TCL1A, TDO2, TEK, TFAP2B, TFPI2, TGFB2, TGFBR3, TGM2, TH, THBD, THY1, TIE1, TIGIT, TLR2, TLR4, TLR9, TMEM119, TMEM255B, TMEM45B, TMEM98, TNFSF8, TNS4, TP63, TPPP3, TPSG1, TREM2, TRH, TRIM29, TRO, TRPC6, TSPAN18, TSPAN7, TUBB2B, TUBB3, TUBB6, TWIST1, TWIST2, UBASH3A, UNC5A, VCAM1, VEGFC, VGLL3, VSIG4, VSNL1, VSTM2L, VTCN1, WFDC1, WNT2, ZFHX4, ZNF683

DCIS1  |  positive: BMPR1B, PITX1, SCGB1D2, OPRD1, SLC16A1, CHAD, EREG, SMPD3, MUC16, KCNF1, PCDH10, ITGB6, AGR2, MUC5B, TMEM45B, DLX4, CA12, GIPC1, S100A1, POTEI, ESR1, SAPCD2, ME1, SLC12A2, CEACAM5, ITGA3, CACNG4, NQO1, ROR2, IGFBP2, TUBB3, SOX9, SLC30A8, REEP6, PRKAR1A, BARX2, G6PD, SOD1, CACNA1D, PIK3R2, CYP27A1, PDE10A, SPA17, VTCN1, P2RY2, AGR3, SDC4, ENTPD2, SPATA18, HK2, C4B, HES6, PLXNB1, RIPK4, NKX3-1, IGSF3, FAAH, CTNND2, NECTIN1, CFB, NTN4, SIAE, DTNA, THSD4, KLF5, SLC7A11, LASP1, FASN, CASZ1, MCM3AP, XBP1, VSTM2L, MACC1, CMYA5, STX1A, ATP7B, RMND1, WFS1, SERPINA5, POTEF, LMX1B, TDO2, SERPINB5, SASH1, KCNK15, TNFRSF12A, SULF2, TRIM29, CGNL1, PLAT, SLC9A3R2, TRPS1, ST14, BAALC, CD151, FOXD2, DNAH7, NECTIN2, CELSR3, DEPTOR, PADI2, VAV3, NINJ1, IL17RB, PRKAA2, ABCA10, NOD2, PRODH, PPFIA3, LRATD2, TUBD1
DCIS1  |  negative: ABCA8, ABCC12, ABCC8, ABCC9, ABI3, ABI3BP, AC007906.2, ACVRL1, ADA, ADAM12, ADAM33, ADAMDEC1, ADAMTS12, ADAMTS14, ADAMTS4, ADAMTS5, ADCYAP1, ADGRE2, ADGRL4, ADORA2B, ADORA3, ADRA1B, ADRA2A, ADRB2, ADTRP, AIM2, ALPL, ANGPT1, ANXA8, APLNR, ASPN, ATL1, AVPR1A, BANK1, BCL11B, BCL2A1, BCL6B, BLK, BMP2, BMP6, BNC2, BPIFB1, BRINP1, C1QTNF1, C3AR1, C7, CACNA1B, CALB2, CALCRL, CARD9, CASP10, CAVIN2, CAVIN3, CCDC102B, CCDC141, CCL14, CCL19, CCN3, CCN4, CCR1, CCR4, CCR5, CD19, CD1C, CD1D, CD1E, CD209, CD22, CD226, CD247, CD27, CD274, CD28, CD302, CD33, CD34, CD38, CD3G, CD40, CD40LG, CD5, CD7, CD72, CD79A, CD79B, CD83, CD86, CDH13, CDH23, CDH5, CDH6, CEP112, CFC1, CHST8, CLDN5, CLEC10A, CLEC12A, CLEC14A, CLEC4C, CLEC5A, CLEC7A, CMA1, CMKLR1, CNR2, CNRIP1, CNTNAP1, COCH, COL10A1, COL4A3, COL4A4, COLEC12, CPVL, CPXM2, CR1, CRB2, CSF2RA, CSF3R, CST7, CTLA4, CTSG, CTSW, CUX2, CXADR, CXCL2, CXCR3, CXCR6, CYSLTR1, DCLK1, DERL3, DIO2, DLK1, DLL4, DNASE1L3, DNM1, DPT, DPY19L2, DSG3, EBF2, EDN1, EDNRA, EGF, EGR2, ENPP2, ENPP3, EPB41L3, EPHB1, ERG, ESM1, F2RL3, F7, FAM107A, FAP, FAT4, FCER1A, FCGR2B, FCMR, FCRL1, FCRL2, FCRL3, FCRL5, FCRLA, FES, FGF1, FGF2, FGF7, FGR, FILIP1L, FLT1, FLT3, FLT4, FOLH1, FOLR2, FOXC2, FPR1, FREM1, FRZB, FSCN1, FUT7, GABRD, GBP5, GHR, GIMAP5, GIMAP8, GJC1, GLI2, GPR174, GPR34, GPR39, GPR4, GRIK4, GRP, GUCY1A2, GZMA, GZMB, GZMK, HAS2, HBEGF, HCK, HDAC9, HDC, HEYL, HGF, HLX, HMCN1, HOXA5, HOXD10, HOXD9, HPGD, HPGDS, HPSE, HPSE2, HS3ST3A1, HSPA6, ICAM2, ICOS, IDO1, IGF1, IL12RB1, IL18R1, IL1RL1, IL21R, IL2RB, IL33, IL3RA, IL6R, INHBA, IRF4, ITGA7, ITIH5, ITK, JAM2, KCNA3, KCNH6, KCNJ8, KCNK10, KCNK17, KCNN3, KDR, KIAA0408, KIT, KLK10, KLK6, KLRD1, KLRG1, LAIR1, LAMP3, LCK, LCN2, LCNL1, LDB3, LEF1, LIF, LILRA4, LILRB2, LILRB4, LILRB5, LMO2, LOX, LPAR1, LPL, LRRC15, LRRK2, LTK, LURAP1, LY96, MAP1A, MAP2, MAP3K7CL, MERTK, MFAP5, MLXIPL, MMP9, MMRN2, MNDA, MPP1, MRC1, MS4A1, MS4A2, MTNR1B, NCCRP1, NCF1, NCF2, NIBAN3, NLRP2, NLRP3, NOS3, NOSTRIN, NPR1, NR4A3, NR5A2, NRN1, NRROS, OLR1, P2RX1, P2RX5, P2RX7, P2RY13, P2RY14, P2RY6, PARP15, PAX5, PCDH17, PCDH18, PCDH8, PDE2A, PDE3A, PDGFD, PDGFRL, PDPN, PGF, PGR, PHEX, PI16, PIEZO2, PILRA, PLA2G7, PLCE1, POTEJ, POU2AF1, POU2F2, PPP1R1C, PRDM6, PRF1, PRICKLE1, PRKCQ, PROCR, PROX1, PTAFR, PTGDR, PTGES, PTGIR, PTGIS, PTGS1, PTPRB, RASSF2, RASSF4, RCAN2, RECK, RELN, RGS5, RIMS3, ROBO4, RUNX1T1, S1PR1, SCIMP, SCN9A, SDC2, SDK2, SELP, SELPLG, SEMA7A, SEPTIN4, SFRP4, SH2D1A, SH2D2A, SH3RF2, SHANK3, SHD, SIGLEC1, SIGLEC6, SIGLEC9, SIM2, SLAMF1, SLAMF6, SLAMF8, SLC11A1, SLC18A2, SLC1A3, SLC35G2, SLC6A1, SLC7A7, SLCO2A1, SLIT3, SMOC2, SNCA, SOD3, SOSTDC1, SOX17, SOX18, SOX5, SPIB, SPOCK1, SPON1, SRPX, STAP1, SUGCT, SVEP1, TAL1, TBX2, TBXA2R, TCL1A, TEK, TFAP2B, TGFB2, TGFBR3, TGM2, TH, THBD, THEMIS, TIE1, TIGIT, TLR1, TLR4, TLR6, TLR7, TLR9, TMEM119, TMEM255B, TNFRSF13B, TNFRSF13C, TNFRSF17, TNFSF8, TPSG1, TREML2, TRH, TRO, TRPC1, TRPC6, TSPAN18, TSPAN7, TUBB2B, TUBB6, TWIST1, TWIST2, UBASH3A, VCAM1, VEGFC, VGLL3, VSIG4, VSNL1, WFDC1, WNT2, WNT4, WNT5B, ZBP1, ZEB1, ZFHX4, ZNF683

DCIS2  |  positive: BPIFB1, PDZK1, DTNA, AGR3, PLAT, BAALC, STEAP4, TSPAN8, PPP1R1B, NAT1, ESR1, EEF1A2, LRG1, PSD3, SIM2, SLC1A1, NPY1R, TRH, KCNK15, CFB, CEACAM5, CLDN7, TUBB3, AGR2, MUC1, CCND1, GREB1, PTK6, NOS1AP, PRLR, F7, EGF, GRIK3, THSD4, NECTIN2, HSPB1, GDF15, SERPINA5, PBX1, LASP1, EMP2, PSMC5, HES4, FSIP1, TPBG, INPP4B, OCLN, PPM1D, NQO1, ERBB3, MAP7, XBP1, TUBB2A, PMAIP1, SEZ6L2, SOX18, ACE, KIAA0408, TRIB3, CACNA1B, TNNT2, MAPK15, FGFR4, CELSR1, CTNND2, PLCE1, PTGES, CFC1, MAPT, FLRT3, AXIN2, LAD1, MACC1, CHAD, PTHLH, TBX3, TFPI2, NEBL, DHCR7, TLK2, FIBCD1, SPAG17, BMP8B, NOSTRIN, CCL22, BCAM, GHR, ADRA2A, OLFM1, TCIM, TNFRSF12A, SYTL2, BDKRB2, RERG, DSG2, EFS, ENPP1, TACC2, IRS1, NLRP2, SLC9A3R1, FRAS1, WNT4, TENT5C, DMKN, EFR3B, KCNK1, SIX1
DCIS2  |  negative: ABCA10, ABCA8, ABCC9, ABI3, ABI3BP, AC007906.2, ACSL4, ACVRL1, ADA, ADAM12, ADAM33, ADAMDEC1, ADAMTS1, ADAMTS12, ADAMTS14, ADAMTS2, ADAMTS4, ADAMTS5, ADCYAP1, ADGRE2, ADGRL4, ADORA2B, ADORA3, ADRA1B, ADRB2, ADTRP, AIM2, ALPL, ANGPT1, ANGPTL2, ANXA8, APLNR, ASPN, ATL1, AVPR1A, BANK1, BCL11B, BCL2A1, BCL6B, BLK, BMP2, BMP6, BMPR1B, BNC2, BRINP1, C1QTNF1, C3AR1, C4B, C7, CADM3, CALB2, CALCRL, CARD9, CAVIN2, CAVIN3, CCDC102B, CCDC141, CCL14, CCL19, CCN3, CCN4, CCR1, CCR4, CCR5, CD19, CD1C, CD1D, CD1E, CD209, CD22, CD226, CD247, CD248, CD27, CD274, CD28, CD302, CD33, CD34, CD38, CD3D, CD3G, CD40, CD40LG, CD5, CD72, CD79A, CD79B, CD86, CD8A, CDH13, CDH23, CDH5, CDH6, CEP112, CES1, CLDN5, CLEC10A, CLEC12A, CLEC14A, CLEC4C, CLEC5A, CLEC7A, CMA1, CMKLR1, CNN1, CNR2, CNRIP1, CNTNAP1, COCH, COL10A1, COL4A3, COL4A4, COLEC12, CPVL, CPXM2, CR1, CRB2, CSF2RA, CSPG4, CST7, CTLA4, CTSG, CTSW, CUX2, CX3CL1, CXCL2, CXCR3, CXCR6, CYSLTR1, DCLK1, DERL3, DIO2, DLK1, DLL4, DLX4, DMD, DNASE1L3, DPT, DPY19L2, DSG3, EBF2, ECRG4, EDNRA, EGR2, ELN, ENPP2, ENPP3, EPHB1, ESM1, ETV1, F2RL3, FAM107A, FAP, FAT2, FAT4, FBLN5, FCER1A, FCGR2B, FCGR3A, FCMR, FCRL1, FCRL2, FCRL3, FCRL5, FCRLA, FES, FGF1, FGF2, FGF7, FGR, FLT1, FLT3LG, FLT4, FOLH1, FOLR2, FOXC2, FOXD2, FPR1, FREM1, FRZB, FSCN1, FUT7, GABRD, GABRP, GBP5, GIMAP5, GIMAP8, GJC1, GLI2, GLT8D2, GPR174, GPR34, GPR4, GRK5, GRM4, GRP, GUCY1A2, GZMA, GZMB, GZMK, HAS2, HBEGF, HCK, HDAC9, HDC, HEYL, HGF, HLX, HMCN1, HOXA5, HOXD10, HOXD9, HPGD, HPGDS, HPSE, HPSE2, HS3ST3A1, ICAM2, IDO1, IGF1, IL12RB1, IL18, IL18R1, IL1RL1, IL21R, IL2RB, IL33, IL34, IL3RA, IL6R, INHBA, IRF4, IRX1, ITGA7, ITIH5, ITK, JAM2, JAM3, KCNA3, KCNF1, KCNJ8, KCNK10, KCNK17, KCNMB1, KCNN3, KDR, KIT, KLHL13, KLK10, KLK6, KLRD1, KLRG1, KLRK1, LAIR1, LAMP3, LCK, LCN2, LCNL1, LDB3, LEF1, LIF, LILRA4, LILRB2, LILRB4, LILRB5, LOX, LPAR1, LPL, LRRC15, LTK, LURAP1, LY75, LY96, MAOB, MAP1A, MAP2, MAP3K7CL, MERTK, MFAP5, MME, MMP9, MMRN2, MNDA, MPP1, MRC1, MS4A1, MS4A2, MUC16, NCCRP1, NCF1, NGFR, NIBAN3, NLRP3, NOD2, NOS3, NPNT, NPR1, NR4A3, NR5A2, NRN1, NRROS, NTRK2, OLR1, OPRD1, P2RX1, P2RX5, P2RX7, P2RY13, P2RY14, P2RY6, PALMD, PAMR1, PARP15, PAX5, PCDH10, PCDH17, PCDH18, PCDH8, PDE10A, PDE2A, PDE3A, PDGFD, PDGFRA, PDGFRB, PDPN, PGF, PGR, PHEX, PI16, PIEZO2, PILRA, PLA2G7, PLAGL1, PLCG2, PLEKHH2, PLXDC1, POTEI, POTEJ, POU2AF1, POU2F2, PPP1R1C, PRF1, PRICKLE1, PRKCQ, PROCR, PROM1, PROX1, PRRX1, PTGDR, PTGIR, PTGIS, PTGS1, PTPRB, RBMS3, RCAN2, RECK, RELN, RGS5, RIMS3, RNF180, ROBO4, ROR2, RTN1, RUNX1T1, S1PR1, SAMD5, SASH1, SATB1, SCGB1D2, SCIMP, SCN9A, SDC2, SDK2, SELP, SEMA7A, SEPTIN4, SERPINB5, SFRP4, SH2D1A, SH3RF2, SHANK3, SHD, SIGLEC1, SIGLEC6, SIGLEC9, SLAMF1, SLAMF6, SLAMF8, SLC11A1, SLC18A2, SLC1A3, SLC35G2, SLC6A1, SLC7A7, SLCO2A1, SLIT2, SMPD3, SNCA, SOD3, SOSTDC1, SOX10, SOX17, SOX5, SPATA18, SPIB, SPOCK1, SPON1, SRPX, STAP1, STAT4, SUGCT, SULF1, SVEP1, SYNPO2, TAL1, TBX2, TBXA2R, TCL1A, TDO2, TEK, TFAP2B, TGFB2, TGFBR3, TH, THBD, THEMIS, TIE1, TIGIT, TLR1, TLR4, TLR6, TLR7, TLR9, TMEM119, TMEM255B, TMEM98, TNFRSF13B, TNFRSF17, TNFSF8, TPPP3, TPSG1, TRBC1, TREM2, TREML2, TRO, TRPC1, TRPC6, TSPAN18, TSPAN7, TUBB2B, TUBB6, TWIST1, TWIST2, UBASH3A, VCAM1, VEGFC, VGLL3, VSIG4, VSNL1, VTCN1, WFDC1, WNT2, WNT5B, ZBP1, ZEB1, ZFHX4, ZNF683

T  |  positive: IL7R, CD2, TRAC, CD96, CD3E, ZAP70, ITK, CD3D, IL2RB, KLRK1, TRBC1, CD6, CD8A, GZMA, THEMIS, CD28, CD5, CD3G, CD247, BCL11B, LCK, STAT4, CCR5, TCF7, GBP5, RASGRP1, ITGAL, SLAMF1, PRDM1, NLRC5, KCNA3, KLRD1, CXCR6, CD40LG, SLFN5, ZNF683, CD7, GIMAP4, CCR4, LAT, SH2D1A, AOAH, PRKCQ, PRF1, CTSW, GZMK, KLRG1, CD226, TIGIT, FYN, TRAF1, LEPROTL1, FLT3LG, JAK3, FCMR, CD52, SP140, ITGB7, CASP8, CST7, CTLA4, SIGIRR, NFATC2, CXCR3, APOBEC3G, ADGRE5, RUNX3, SLAMF6, GPR174, CD27, ZBP1, ATM, SYNRG, BTN3A1, SEMA4D, STK4, UBASH3A, PTGDR, GIMAP5, SH2D2A, TNFSF8, SELPLG, SATB1, CDC14A, CD44, PBXIP1, RBL2, CCND2, NLRP1, PARP15, KLF12, KAT2B, LEF1, PTGER4, ICOS, TC2N, LPXN, PTK2B, TNFAIP8, TUBA4A, OGT, TMEM173, IL21R, IL12RB1, S1PR1, LY75, LBH, CD58, CFLAR, SMCHD1, CD47, CYLD
T  |  negative: ABCA1, ABCA10, ABCA8, ABCC3, ABCC8, ABCC9, ABI3BP, AC007906.2, ACVRL1, ADAM12, ADAM28, ADAM33, ADAMDEC1, ADAMTS1, ADAMTS12, ADAMTS14, ADAMTS2, ADAMTS4, ADAMTS5, ADCYAP1, ADGRB2, ADGRE2, ADGRL4, ADORA2B, ADORA3, ADRA1B, ADRA2A, AEBP1, AFAP1L2, AGAP1, AGR3, AIF1, AIM2, ALDH2, ALDH7A1, ALPK3, ALPL, ANGPT1, ANGPTL2, ANGPTL4, ANO1, ANXA3, ANXA8, AP1S3, APLNR, AQP1, AR, ASPN, ATP7B, AVPR1A, BAALC, BACE2, BANK1, BCL6B, BDKRB2, BGN, BLK, BLNK, BMP2, BMP2K, BMP6, BMPR1B, BNC2, BPIFB1, BRINP1, BRIP1, C1QC, C1QTNF1, C3AR1, C5AR1, C7, CA12, CACNA1B, CACNA1D, CACNG4, CADM3, CALB2, CALCRL, CALML3, CARD10, CARD14, CARD9, CAVIN2, CAVIN3, CCDC102B, CCDC170, CCDC50, CCDC80, CCL14, CCN1, CCN2, CCN4, CCR1, CD14, CD163, CD19, CD1C, CD1D, CD1E, CD209, CD22, CD248, CD274, CD302, CD33, CD34, CD38, CD40, CD68, CD79A, CD79B, CD83, CD86, CDH11, CDH13, CDH3, CDH5, CDH6, CDK14, CEACAM5, CEBPA, CELSR1, CEP112, CES1, CFB, CFC1, CGNL1, CHAD, CHST8, CLCN5, CLDN5, CLEC10A, CLEC12A, CLEC14A, CLEC4C, CLEC5A, CLEC7A, CLGN, CLIC3, CMA1, CMKLR1, CMYA5, CNN1, CNR2, CNRIP1, CNTNAP1, COCH, COL10A1, COL17A1, COL18A1, COL4A3, COL4A4, COL5A1, COL5A2, COL7A1, COLEC12, COMP, CPVL, CPXM2, CR1, CRB2, CSF1R, CSF2RA, CSF3R, CSPG4, CTHRC1, CTNND2, CTSG, CTSL, CUX2, CX3CL1, CXADR, CXCL12, CXCL16, CXCL2, CYP24A1, CYP27A1, DAB2, DCLK1, DDR2, DEPTOR, DERL3, DKK3, DLK1, DLL4, DLX4, DMD, DMKN, DMXL2, DNAH7, DNASE1L3, DNM1, DPY19L2, DSC3, DSG2, DSG3, DTNA, DUSP5, EBF2, ECRG4, EDNRA, EEF1A2, EFNB2, EFR3B, EFS, EGF, EGFL7, EGFR, ELN, EMP1, EMP2, ENG, ENPP1, ENPP2, ENPP3, ENTPD2, EPAS1, EPB41L3, EPHA2, EPHB1, ERBB3, EREG, ERG, ERRFI1, ESM1, ESR1, EXO1, F13A1, F2RL3, F3, F7, FAAH, FAM107A, FAP, FAT2, FAT4, FBLN2, FBLN5, FBP1, FCER1A, FCGR2A, FCGR2B, FCGR3A, FCRL1, FCRL2, FCRL5, FCRLA, FES, FGF1, FGF2, FGF7, FGFR2, FGFR3, FGFR4, FHL2, FIBCD1, FILIP1L, FLRT3, FLT1, FLT3, FMN1, FOLH1, FOLR2, FOXC2, FPR1, FRAS1, FREM1, FRMD6, FRZB, FSCN1, FSIP1, FST, FUT7, FZD4, GABRD, GABRP, GAS6, GATA2, GHR, GJC1, GLI2, GLT8D2, GPR34, GPR39, GPR4, GPRC5B, GRAMD2B, GREB1, GREB1L, GRIK3, GRIK4, GRM4, GRP, GUCY1A2, GXYLT2, GZMB, HAS2, HAVCR2, HBEGF, HCK, HDAC9, HDC, HES4, HEYL, HGF, HK2, HLX, HMCN1, HMOX1, HOXA5, HOXD10, HOXD9, HPGD, HPGDS, HPSE2, HS6ST2, HSPA6, HSPG2, HTRA1, IDO1, IGF1, IGSF3, IL18, IL18R1, IL1RL1, IL33, IL34, IL3RA, INHBA, IRF4, IRS1, IRS2, IRX1, ITGA2, ITGA2B, ITGA3, ITGA7, ITGB6, ITIH5, JAG1, JAM2, KANK2, KCNF1, KCNH6, KCNJ8, KCNK1, KCNK10, KCNK15, KCNK17, KCNMA1, KCNMB1, KCNN3, KDR, KIF18B, KIT, KLF5, KLK10, KLK6, KMO, LAD1, LAMB3, LAMC1, LAMC2, LAMP3, LCN2, LCNL1, LDB3, LGMN, LGR4, LIF, LILRA4, LILRB2, LILRB4, LILRB5, LMO2, LMX1B, LOX, LOXL1, LOXL2, LPAR1, LPL, LRP1, LRRC15, LRRK2, LTBP2, LTK, LUM, LURAP1, LY96, MACC1, MAFB, MAOB, MAP1A, MAP1B, MAP2, MAP3K20, MAP7, MAPK15, MAPT, ME1, MECOM, MEIS1, MEIS2, MERTK, MET, MFAP5, MLF1, MLXIPL, MME, MMP14, MMP9, MMRN2, MNDA, MNX1, MPP1, MRC1, MRC2, MS4A1, MS4A2, MS4A6A, MSR1, MST1R, MTNR1B, MUC16, MUC5B, MYCL, MYCN, MYH11, MYLK, MYO1B, MYO1E, NAT1, NCCRP1, NCF1, NCF2, NEBL, NECTIN1, NEK2, NGFR, NIBAN3, NOS1AP, NOSTRIN, NOTCH3, NPNT, NPR1, NPY1R, NR4A3, NR5A2, NRN1, NRP1, NRP2, NTN4, NTRK2, OCLN, OLFM1, OLFML3, OLR1, OPRD1, OVOL2, OXTR, P2RX1, P2RY13, P2RY14, P2RY2, P2RY6, PADI2, PALMD, PAMR1, PARM1, PAX5, PBK, PCDH10, PCDH17, PCDH18, PCDH8, PDE10A, PDE2A, PDE3A, PDE5A, PDGFA, PDGFRA, PDGFRB, PDGFRL, PDLIM4, PDPN, PDZK1, PGF, PGR, PHEX, PHF21B, PHGDH, PIEZO2, PILRA, PITX1, PLA2G7, PLA2R1, PLAGL1, PLAT, PLAUR, PLCE1, PLCG2, PLEKHH2, PLTP, PLVAP, PLXNB1, PMAIP1, PODXL, POTEF, POTEI, POU2AF1, PPARG, PPM1H, PPP1R1B, PRDM6, PRICKLE1, PRKAA2, PRLR, PROCR, PRODH, PROM1, PROX1, PRRX1, PSD3, PTAFR, PTGES, PTGFRN, PTGIR, PTGIS, PTGS1, PTK6, PTK7, PTPRB, PTPRG, RAPGEF3, RAPGEF5, RASD1, RASSF4, RBMS3, RBPMS, RCAN2, RECQL4, REEP6, RELN, RERG, RET, RGL3, RGS5, RIMS3, RIPK4, RNF180, RNF39, RNF43, ROBO4, ROR2, RRM2, RTN1, RUNX1T1, S100A1, S1PR3, SAMD5, SAPCD2, SASH1, SCGB1D2, SCIMP, SCN9A, SDC2, SDC4, SDK2, SEMA4B, SEMA7A, SEPTIN4, SEPTIN5, SERPINA5, SERPINB5, SERPINE1, SERPINH1, SEZ6L2, SFRP1, SGK1, SH3RF2, SHANK3, SHD, SIGLEC1, SIGLEC6, SIGLEC9, SIM2, SIRPA, SIX1, SLAMF8, SLC11A1, SLC16A1, SLC18A2, SLC1A1, SLC1A3, SLC2A9, SLC30A8, SLC35G2, SLC6A1, SLC7A11, SLC7A7, SLC9A3R2, SLCO2A1, SLCO2B1, SLIT2, SLIT3, SMOC2, SMPD3, SMTN, SNCA, SOD3, SORCS1, SOSTDC1, SOX10, SOX17, SOX18, SOX5, SOX9, SPAG17, SPATA18, SPECC1, SPIB, SPOCK1, SPON1, SPON2, SPRY2, SRPX, ST14, STAP1, STEAP1, STEAP4, SULF1, SULF2, SVEP1, SYNM, SYNPO2, TACC2, TAL1, TBC1D30, TBX2, TBX3, TBXA2R, TCIM, TCL1A, TDO2, TEK, TFAP2B, TFPI2, TGFB2, TGM2, THBD, THBS2, THSD4, THY1, TIE1, TIMP3, TLR1, TLR2, TLR4, TLR6, TLR7, TLR9, TMEM119, TMEM255B, TMEM97, TMEM98, TNC, TNFRSF12A, TNFRSF13B, TNFRSF13C, TNFRSF17, TNFRSF21, TNFSF13, TNNT2, TNS4, TNXB, TP63, TPBG, TPPP3, TPSG1, TREM2, TREML2, TRH, TRIM29, TRO, TRPC6, TRPM2, TSPAN7, TSPAN8, TUBB2A, TUBB2B, TUBB3, TUBB6, TWIST1, TWIST2, TYMS, UNC5A, VCAM1, VCL, VEGFC, VGLL3, VSIG4, VSNL1, VTCN1, WDR90, WFDC1, WFS1, WNT2, WNT5B, ZFAT, ZFHX4, ZNF296

dendritic  |  positive: LCNL1, CLEC4C, GZMB, CSF2RA, DNASE1L3, IL3RA, SCN9A, EPHB1, SHD, LILRA4, CLIC3, PHEX, FLT3, LAMP3, IDO1, CLCN5, TCL1A, RUNX2, FUT7, ZFAT, IRF7, DUSP5, LTK, TLR9, TLR7, SPIB, CCDC50, CD83, NIBAN3, IRF4, CUX2, SMPD3, MAP1A, KCNK10, P2RY14, KCNK17, CMKLR1, NR4A3, RASSF4, MYCL, RIMS3, MS4A6A, SELPLG, CXCR3, ZDHHC17, ERN1, MAP3K14, TRAF1, RASSF2, DERL3, BLK, RELT, BLNK, LILRB4, RASD1, HS3ST3A1, SLC7A11, FCER1A, LAIR1, IL6R, LY75, IL7R, ADA, SEMA7A, P2RX1, NCCRP1, EPHA2, CTSC, SLC7A5, KCTD5, MAPKAPK2, JAK2, PLXNC1, IFI44L, NCF1, TNFRSF17, MX1, SULF2, P2RY6, FSCN1, CD274, HCK, GAS6, VEGFB, HLX, KIF2A, ETV6, IL21R, TREML2, CYSLTR1, WNT5B, KAT2B, PTGIR, TUBB6, TNFRSF21, ALDH2, MYO1E, NPC1, FES, CD68, ADAM12, CCL19, ZNF296, ZBTB33, RFTN1, MAN2B1, OAS1, IFNAR2, TSPAN13, TRAF4, HDAC9, CCDC102B, MDFIC, ZEB1, CST7, PLEKHO1, SLC2A1, RUBCN
dendritic  |  negative: ABCA10, ABCA8, ABCC12, ABCC8, ABCC9, ABI3, ABI3BP, AC007906.2, ACVRL1, ADAM33, ADAMTS1, ADAMTS12, ADAMTS14, ADAMTS2, ADAMTS4, ADAMTS5, ADCYAP1, ADGRB2, ADGRL4, ADORA2B, ADORA3, ADRA1B, ADRA2A, AFAP1L2, AGR3, AIM2, ALPK3, ALPL, ANGPT1, ANGPTL2, ANGPTL4, ANXA3, ANXA8, APLNR, AQP1, ASPN, ATL1, AURKB, AVPR1A, BAALC, BACE2, BARX2, BCL6B, BDKRB2, BMP2, BMP6, BMP8B, BMPR1B, BNC2, BPIFB1, BRINP1, C1QTNF1, C3AR1, C5AR1, C7, CA12, CACNA1B, CACNG4, CALB2, CALML3, CARD10, CARD14, CARD9, CAVIN2, CAVIN3, CCDC141, CCL14, CCN3, CCN4, CCR4, CD19, CD1C, CD1D, CD209, CD22, CD247, CD27, CD28, CD33, CD34, CD3G, CD40LG, CD72, CD79B, CDH13, CDH3, CDH5, CDH6, CDKN2B, CEBPA, CEP112, CES1, CFC1, CGNL1, CHST8, CLDN5, CLEC12A, CLEC14A, CLEC5A, CLEC7A, CLGN, CMA1, CMYA5, CNN1, CNRIP1, CNTNAP1, COL10A1, COL17A1, COL4A3, COL4A4, COLEC12, COMP, CPXM2, CR1, CRB2, CSF1, CSPG4, CTLA4, CTNND2, CTSG, CX3CL1, CXADR, CXCL2, CXCR6, CYP27A1, DCLK1, DDR2, DIO2, DKK3, DLK1, DLL4, DLX4, DNAH5, DNAH7, DPT, DPY19L2, DSC3, DSG3, EBF2, ECRG4, EDN1, EDNRA, EFR3B, EGF, EGFR, EGR2, ELN, ENPP3, ENTPD2, EREG, ERG, ERRFI1, ESM1, ETV1, F2R, F2RL3, F3, F7, FAM107A, FAT2, FAT4, FBLN5, FCGR2A, FCGR2B, FCGR3A, FCMR, FCRL1, FCRL2, FCRL5, FCRLA, FGF1, FGF2, FGF7, FGFR2, FGFR4, FILIP1L, FLRT3, FLT1, FLT3LG, FLT4, FMOD, FOLH1, FOLR2, FOXD2, FPR1, FRAS1, FREM1, FRZB, FST, GABRD, GABRP, GHR, GIMAP5, GJC1, GLT8D2, GPR34, GPR4, GPRC5B, GRIK3, GRK5, GRP, GUCY1A2, GZMA, GZMK, HAS2, HBEGF, HDC, HES6, HEYL, HGF, HMCN1, HOXA5, HOXD10, HOXD9, HPGD, HPGDS, HPSE, HPSE2, HS6ST2, HSPA6, ICOS, IGF1, IGSF3, IL17RB, IL18, IL1RL1, IL2RB, IL33, IL34, INHBA, IRS1, IRX1, ITGA2B, ITGA7, ITIH5, ITK, JAM2, JAM3, KANK2, KCNF1, KCNH6, KCNJ8, KCNK15, KCNMB1, KCNN3, KCNN4, KDR, KIAA0408, KIF18B, KIT, KLK10, KLK6, KLRD1, LAMB3, LAMC2, LCK, LCN2, LDB3, LEF1, LGR4, LIF, LILRB5, LMO2, LOX, LOXL2, LPAR1, LRG1, LRRC15, LURAP1, MAOB, MAP1B, MAP2, MAP3K7CL, MAPK11, MAPK15, MAPT, ME1, MECOM, MEIS2, MERTK, MET, MFAP5, MLXIPL, MME, MMP9, MMRN2, MNX1, MRC1, MS4A1, MS4A2, MSR1, MTNR1B, MUC16, MUC5B, MYH11, MYLK, NAT1, NEK2, NLRP3, NOS1AP, NOS3, NOSTRIN, NPNT, NPR1, NPY1R, NR5A2, NRN1, NTRK2, OLFM1, OLFML3, OLR1, OPRD1, OXTR, P2RX5, P2RY2, PALMD, PAMR1, PAX5, PBK, PCDH10, PCDH17, PCDH18, PDE10A, PDE2A, PDE3A, PDE5A, PDGFA, PDGFD, PDGFRA, PDGFRL, PDLIM4, PDPN, PDZK1, PGF, PGR, PI16, PILRA, PITX1, PLA2G7, PLA2R1, PLCE1, PLEKHH2, PODXL, POTEI, POTEJ, POU2AF1, PPARG, PPFIA3, PRDM6, PRF1, PRICKLE1, PRKAA2, PRKCQ, PROCR, PROM1, PROX1, PRRX1, PTAFR, PTGIS, PTGS1, PTHLH, PTPRB, PTPRM, RAPGEF3, RASGRP1, RBPMS, RCAN2, RECK, RELN, RERG, RGS5, RIPK4, RNF180, RNF39, ROBO4, ROR2, RTN1, RUNX1T1, S100A1, SAMD5, SASH1, SCGB1D2, SCIMP, SDC2, SELP, SEPTIN4, SEPTIN5, SERPINA5, SERPINB5, SERPINE1, SFRP1, SFRP4, SH2D1A, SH2D2A, SH3RF2, SHANK3, SIGLEC1, SIGLEC9, SIM2, SLAMF8, SLC11A1, SLC18A2, SLC1A3, SLC35G2, SLC6A1, SLC7A7, SLCO2A1, SLIT2, SLIT3, SMOC2, SMTN, SOD3, SORCS1, SOSTDC1, SOX10, SOX17, SOX18, SOX5, SOX9, SPA17, SPAG17, SPATA18, SPOCK1, SPON1, SPRY2, STAP1, STEAP1, STEAP4, STX1A, SUGCT, SULF1, SVEP1, TAL1, TBX2, TBXA2R, TDO2, TEK, TFAP2B, TGFB2, TGFBR3, TGM2, TH, TICRR, TIE1, TIGIT, TLR4, TMEM119, TMEM255B, TMEM45B, TMEM98, TNFRSF12A, TNFRSF13B, TNFRSF13C, TNS4, TP63, TPPP3, TPSG1, TREM2, TRIB3, TRIM29, TRPC1, TRPC6, TSPAN18, TSPAN7, TUBB3, TWIST1, TWIST2, VEGFC, VGLL3, VSIG4, VSNL1, VSTM2L, VTCN1, WNT2, WNT4, WWC1, ZBP1, ZFHX4, ZNF683

endo  |  positive: SOX17, CDH5, HOXD9, NR5A2, SLCO2A1, PLVAP, PTPRB, CCL14, ROBO4, CLEC14A, ADGRL4, SHANK3, KDR, FLT1, CD34, CALCRL, TEK, ENG, CLDN5, HSPG2, BCL6B, PCDH17, NPR1, MMRN2, DLL4, AQP1, PALMD, APLNR, SLC35G2, NRN1, PROX1, FOLH1, TAL1, TSPAN7, TMEM255B, PDE2A, TIE1, NOSTRIN, EGFL7, CAVIN2, CDH13, HOXD10, TSPAN18, FLT4, ACVRL1, ADAMTS4, EMP1, IL33, F2RL3, IL3RA, SELP, CPXM2, FAM107A, ESM1, THBD, BMP2, NOS3, GPR4, PTPRM, HBEGF, TGM2, FAT4, GABRD, PODXL, FZD4, SOX18, PIEZO2, TBXA2R, ADAMTS1, JAM2, RGS5, MET, VEGFC, ANGPTL2, RELN, S1PR1, BMP6, KCNN3, EPAS1, CAVIN3, ITGA5, NRP2, CNRIP1, C1QTNF1, EDN1, GJC1, ALPK3, RAPGEF3, HTRA1, TGFBR3, ERG, ICAM2, ABI3, LOX, PROCR, MECOM, RCAN2, CEP112, PDE10A, BDKRB2, PGF, ITIH5, GIMAP8, PRCP, PDGFD, EFNB2, HLX, C7, TNXB, RNF180, TRPC6, SASH1, ABCC9, OLFM1, PPARG, GPRC5B, PDGFB, FGF2, CARD10, NPNT, RAPGEF5, FSCN1, LMO2, TGFBR2, ZEB1, ELK3, CXCL12, GIMAP4, ELN, DOCK9, MAPK11
endo  |  negative: ABCA10, ABCA8, ABCC12, ABCC8, AC007906.2, ADAMDEC1, ADAMTS14, ADCYAP1, ADGRE2, ADORA2B, ADORA3, ADRA1B, ADTRP, AGR3, ANGPT1, ANXA8, ASPN, ATL1, BAALC, BANK1, BARX2, BCL2A1, BLK, BLNK, BMP8B, BMPR1B, BPIFB1, BRINP1, C3AR1, C4B, C5AR1, CACNG4, CADM3, CALB2, CALML3, CARD9, CCL22, CCN4, CCR1, CCR4, CCR5, CCR6, CD19, CD1C, CD1E, CD209, CD22, CD226, CD247, CD27, CD28, CD33, CD38, CD3G, CD40LG, CD5, CD7, CD72, CD79A, CD83, CD86, CD8A, CEBPA, CES1, CFC1, CLEC10A, CLEC12A, CLEC4C, CLEC5A, CLEC7A, CLGN, CLIC3, CMA1, CMKLR1, CMYA5, CNR2, COCH, COL10A1, COL4A3, COL4A4, CR1, CRB2, CSF3R, CST7, CTLA4, CTNND2, CTSG, CTSW, CUX2, CXADR, CXCR3, CXCR6, DCLK1, DERL3, DLK1, DLX4, DNAH7, DNM1, DPT, DSC3, DSG3, ECRG4, EGF, EGR2, ENPP3, ENTPD2, EREG, F7, FAT2, FCER1A, FCGR2A, FCGR2B, FCRL1, FCRL2, FCRL3, FCRL5, FCRLA, FGF1, FGFR2, FLRT3, FLT3, FOLR2, FPR1, FREM1, FST, FUT7, GABRP, GLI2, GPR174, GPR39, GRIK3, GRIK4, GRM4, GRP, GZMA, GZMB, GZMK, HAS2, HAVCR2, HCK, HDC, HGF, HPGD, HPGDS, HPSE, HPSE2, HS3ST3A1, ICOS, IDO1, IL12RB1, IL18, IL18R1, IL1RL1, IL21R, IL2RB, IL34, IL6R, IRF4, IRS1, IRS2, IRX1, ITGA2B, ITK, KCNA3, KCNF1, KCNH6, KCNK10, KCNK15, KCNK17, KCNMA1, KCNN4, KIAA0408, KIF18B, KIT, KLHL13, KLK10, KLK6, KLRD1, KMO, LAMB3, LAMC2, LAMP3, LCN2, LCNL1, LDB3, LGR4, LIF, LILRA4, LILRB2, LILRB4, LILRB5, LPAR1, LRG1, LRRK2, LTK, MAOB, MAP1A, MAPT, ME1, MLXIPL, MME, MNDA, MNX1, MS4A1, MS4A2, MTNR1B, MUC16, MUC5B, MYCL, NCCRP1, NCF1, NCF2, NEK2, NIBAN3, NKX3-1, NLRP2, NLRP3, NOD2, NOS1AP, NPY1R, NR4A3, NTRK2, OLR1, OPRD1, P2RX1, P2RX5, P2RX7, P2RY13, P2RY14, PAX5, PCDH10, PCDH8, PDE3A, PDZK1, PGR, PHEX, PHF21B, PI16, PILRA, PLA2G7, PLEKHH2, PMAIP1, POTEI, POTEJ, POU2AF1, POU2F2, PPFIA3, PPP1R1C, PRF1, PRKCQ, PRODH, PROM1, PTGDR, PTGES, PTGIS, PTGS1, PTHLH, RIMS3, RIPK4, RNF39, ROR2, RTN1, S100A1, SCGB1D2, SCIMP, SCN9A, SDK2, SERPINB5, SFRP4, SH2D1A, SH2D2A, SH3RF2, SHD, SIGLEC1, SIGLEC6, SIGLEC9, SIM2, SLAMF6, SLAMF8, SLC11A1, SLC18A2, SLC1A3, SLC6A1, SLC7A11, SLIT2, SMPD3, SOSTDC1, SOX10, SPA17, SPAG17, SPATA18, SPIB, SPOCK1, SPON1, STAP1, STEAP1, SUGCT, TCL1A, TFAP2B, TH, TIGIT, TLR1, TLR6, TLR7, TLR9, TNFRSF13B, TNFRSF13C, TNFRSF17, TNFSF8, TNS4, TP63, TPSG1, TREM2, TREML2, TRH, TRIM29, TRPM2, TUBB2B, TWIST1, TWIST2, UBASH3A, VGLL3, VSIG4, VSNL1, VSTM2L, VTCN1, WNT2, WNT4, WNT5B, ZBP1, ZFHX4, ZNF296, ZNF683

macro  |  positive: SIGLEC1, MS4A6A, SLC11A1, CD163, LILRB5, PLA2G7, CSF1R, MSR1, CSF3R, VSIG4, CLEC7A, SLC1A3, CPVL, CD14, FCGR3A, MRC1, CD68, OLR1, C1QC, CLEC10A, P2RY13, SLAMF8, FOLR2, CD209, F13A1, MERTK, AIF1, FCGR2A, NCF2, HSPA6, MNDA, CD86, CSF2RA, C3AR1, CLEC5A, TLR2, SIGLEC9, RASSF4, SLCO2B1, CD33, FPR1, HAVCR2, AOAH, ADORA3, PLTP, CLEC12A, LILRB2, IL18, ADGRE2, TREM2, NLRP3, GPR34, HCK, PILRA, MPP1, PTGS1, CR1, CD1D, TLR1, FCGR2B, CCR1, SLC7A7, CD1E, DAB2, LILRB4, CMKLR1, TLR4, FCER1A, ADAMDEC1, GIMAP8, HPSE, P2RX7, FGR, LY96, SCIMP, TLR7, CD1C, FMN1, CARD9, RTN1, TNFSF13, KCNMB1, C5AR1, LAIR1, CTSC, PTAFR, HMOX1, TLR6, ADAM28, LRRK2, ABI3, CD72, MAFB, P2RY6, EPB41L3, LGMN, SIRPA, DMXL2, MMP9, FES, ARRB2, COLEC12, NRROS, CASP1, BCL2A1, LPAR6, SLC2A9, KCNMA1, CTSL, LRP1, CEBPA, HGF, CD302, TYMP, CXCL16, TRPM2, LIPA, CREG1, ENPP2, VSIR, CD38, SNX10
macro  |  negative: ABCA10, ABCA8, ABCC12, ABCC8, ABCC9, ABI3BP, AC007906.2, ACVRL1, ADAM12, ADAM33, ADAMTS1, ADAMTS12, ADAMTS14, ADAMTS2, ADAMTS4, ADAMTS5, ADCYAP1, ADGRB2, ADGRL4, ADRA1B, ADRA2A, AFAP1L2, AGR3, ALDH7A1, ALPK3, ALPL, ANGPT1, ANGPTL2, ANXA3, ANXA8, APLNR, AQP1, ASPN, ATL1, AVPR1A, AXIN2, BAALC, BACE2, BANK1, BARX2, BCL6B, BDKRB2, BLK, BMP2, BMP6, BMP8B, BMPR1B, BPIFB1, BRINP1, C1QTNF1, C4B, C7, CACNA1B, CACNG4, CADM3, CALB2, CALCRL, CALML3, CARD10, CAVIN2, CAVIN3, CCDC102B, CCL14, CCL19, CCL28, CCN2, CCN3, CCN4, CCR4, CD19, CD248, CD27, CD34, CD40LG, CD7, CD79A, CD79B, CD8A, CDH11, CDH13, CDH3, CDH5, CDH6, CDKN2B, CELSR3, CENPA, CEP112, CGNL1, CHAD, CLDN5, CLEC14A, CLEC4C, CLGN, CLIC3, CMA1, CMYA5, CNN1, CNR2, CNTNAP1, COCH, COL10A1, COL17A1, COL4A3, COL4A4, COL7A1, COMP, CPXM2, CRB2, CSPG4, CTLA4, CTNND2, CTSG, CTSW, CUX2, CX3CL1, CXCR3, CXCR6, DCLK1, DDR2, DERL3, DIO2, DKK3, DLK1, DLL4, DLX4, DMD, DNAH5, DNAH7, DNASE1L3, DPT, DPY19L2, DSC3, DSG3, EBF2, ECRG4, EDN1, EDNRA, EFNB2, EFR3B, EGF, EGFR, ELN, ENPP3, ENTPD2, EPHA2, EPHB1, EREG, ERG, ERRFI1, ESM1, ETV1, F2R, F2RL3, F3, F7, FAM107A, FAP, FAT2, FAT4, FBLN5, FCRL1, FCRL2, FCRL3, FCRL5, FCRLA, FGF1, FGF2, FGF7, FGFR2, FGFR4, FLRT3, FLT1, FLT4, FMOD, FOLH1, FOXC2, FOXD2, FREM1, FRZB, FST, FZD4, GABRD, GABRP, GHR, GJC1, GLI2, GLT8D2, GPR174, GPR4, GRIK3, GRIK4, GRM4, GRP, GUCY1A2, GXYLT2, GZMB, GZMK, HAS2, HDC, HEYL, HMCN1, HOXA5, HOXD10, HOXD9, HPGD, HPSE2, HS3ST3A1, HS6ST2, ICOS, IGSF3, IL17RB, IL18R1, IL1RL1, IL33, IL34, IL3RA, INHBA, IRF4, IRS1, IRX1, ITGA2B, ITGA7, ITIH5, JAG1, JAM2, JAM3, KANK2, KCNF1, KCNH6, KCNJ8, KCNK10, KCNK15, KCNK17, KCNN3, KDR, KIAA0408, KIF18B, KIT, KLHL13, KLK10, KLK6, KLRD1, LAMB3, LAMC2, LAMP3, LCN2, LCNL1, LDB3, LEF1, LGR4, LIF, LILRA4, LOX, LOXL2, LRG1, LRRC15, LTK, MAOB, MAP1A, MAP1B, MAP2, MAP3K7CL, MAPK11, MAPT, MECOM, MEIS2, MET, MFAP5, MLXIPL, MME, MMP11, MMRN2, MS4A1, MS4A2, MTNR1B, MUC16, MUC5B, MYH11, NAV2, NCCRP1, NGFR, NIBAN3, NKX3-1, NLRP2, NOS1AP, NOS3, NOSTRIN, NPNT, NPR1, NPY1R, NR4A3, NR5A2, NRN1, NTRK2, OPRD1, OXTR, P2RX1, P2RX5, P2RY2, PALMD, PAMR1, PAX5, PBK, PCDH10, PCDH17, PCDH18, PCDH8, PDE10A, PDE2A, PDE3A, PDE5A, PDGFD, PDGFRA, PDGFRB, PDGFRL, PDLIM4, PDPN, PDZK1, PGF, PGR, PHEX, PHF21B, PHLDB2, PI16, PIEZO2, PITX1, PLA2R1, PLCE1, PLEKHH2, PODXL, POTEI, POTEJ, POU2AF1, PPP1R1C, PRF1, PRICKLE1, PRKAA2, PRKCQ, PROM1, PROX1, PRRX1, PTGDR, PTGES, PTGIS, PTHLH, PTK7, PTP4A3, PTPRB, RAPGEF3, RASD1, RBMS3, RBPMS, RCAN2, RECK, RELN, RERG, RGS5, RIMS3, RIPK4, RNF180, RNF39, ROBO4, ROR2, RUNX1T1, S100A1, S1PR1, SAMD5, SCGB1D2, SDK2, SELP, SEMA7A, SEPTIN4, SEPTIN5, SERPINA5, SERPINB5, SERPINE1, SFRP1, SFRP4, SH2D1A, SH2D2A, SH3RF2, SHANK3, SHD, SIGLEC6, SIM2, SLAMF6, SLC18A2, SLC35G2, SLC6A1, SLC7A11, SLCO2A1, SLIT2, SLIT3, SMOC2, SMPD3, SOD3, SORCS1, SOSTDC1, SOX10, SOX17, SOX18, SOX5, SOX9, SPA17, SPAG17, SPATA18, SPIB, SPOCK1, SPON1, SPON2, SRPX, STAP1, STEAP1, STEAP4, SUGCT, SULF1, SVEP1, SYNPO2, TAL1, TBX2, TBXA2R, TCL1A, TDO2, TEK, TFAP2B, TGFBR3, TH, THBS2, THY1, TIE1, TIGIT, TLR9, TMEM119, TMEM45B, TMEM98, TNFRSF13B, TNFRSF13C, TNFRSF17, TNS4, TP63, TPPP3, TPSG1, TRH, TRIB3, TRIM29, TRO, TRPC1, TSPAN18, TSPAN7, TUBB2B, TUBB3, TUBB6, TWIST1, TWIST2, UBASH3A, UNC5A, VEGFC, VGLL3, VSNL1, VSTM2L, VTCN1, WFDC1, WNT2, WNT4, WNT5B, ZFAT, ZFHX4, ZNF296, ZNF683

mast  |  positive: HDC, IL1RL1, KIT, MS4A2, SLC18A2, ADCYAP1, TPSG1, CTSG, HPGDS, LIF, CMA1, HPGD, ENPP3, GATA2, ADGRE2, IL18R1, CSF1, ADRB2, KLRG1, PTGS1, SIGLEC6, FER, P2RX1, STXBP5, BMP2K, ACSL4, FOXP1, MCTP2, ABCC1, TMEM255B, CAVIN2, MAOB, CD33, C3AR1, MEIS2, CD274, AGAP1, LAT, CD22, CALB2, PLAUR, AHR, ABCA1, AP1S3, CD82, IRS2, SGK1, BACE2, PPM1H, CD44, NEK6, ABCC4, ARRB2, ADGRE5, RASSF5, CNST
mast  |  negative: ABCA10, ABCC12, ABCC3, ABCC9, ABI3, ABI3BP, AC007906.2, ACVRL1, ADA, ADAM28, ADAM33, ADAMTS1, ADAMTS12, ADAMTS2, ADAMTS4, ADGRL4, ADORA2B, ADRA1B, ADTRP, AEBP1, AFAP1L2, AIF1, AIM2, ALDH2, ALDH7A1, ALPK3, ANGPTL2, ANGPTL4, ANKRD13A, ANO1, AOAH, APLNR, AQP1, ATL1, AURKB, AVPR1A, AXIN2, BANK1, BARX2, BCL11B, BCL6B, BGN, BLK, BLNK, BMP8B, BMPR1B, BPIFB1, C1QC, C1QTNF1, C5AR1, CACNA1B, CACNG4, CALCRL, CALML3, CARD14, CASP1, CAVIN3, CCDC102B, CCDC170, CCDC50, CCDC80, CCL14, CCN1, CCN2, CCN4, CCR1, CCR5, CD14, CD151, CD163, CD19, CD1D, CD2, CD209, CD247, CD248, CD27, CD28, CD34, CD3D, CD3E, CD3G, CD40, CD5, CD6, CD79A, CD79B, CD86, CD8A, CD96, CDH11, CDH13, CDH23, CDH3, CDH5, CDH6, CDK14, CDKN2B, CEBPA, CELSR1, CFB, CHAD, CLCN5, CLDN5, CLEC10A, CLEC14A, CLEC4C, CLEC7A, CLGN, CLIC3, CLSTN3, CMKLR1, CMYA5, CNN1, COL10A1, COL17A1, COL4A5, COL5A1, COL5A2, COL7A1, COLEC12, COMP, CPVL, CPXM2, CR1, CSF1R, CSF2RA, CSF3R, CSPG4, CTHRC1, CTNND2, CTSC, CTSK, CTSL, CX3CL1, CXCL12, CXCR3, CYP1B1, CYP27A1, DAB2, DCLK1, DCN, DERL3, DIO2, DKK3, DLK1, DLL4, DMD, DMKN, DNAH5, DNASE1L3, DNM1, DOCK9, DSG3, DTNA, DUSP5, EBF2, ECRG4, EDN1, EDNRA, EFNB2, EFR3B, EFS, EGF, EGFL7, EGFR, ELK3, ELN, EMP1, EMP2, ENPP1, ENTPD2, EPB41L3, EPHA2, EPHB1, ERRFI1, ESR1, F13A1, F2R, F3, F7, FAAH, FAP, FAT2, FBLN2, FBLN5, FCGR2A, FCGR2B, FCGR3A, FCMR, FCRL1, FCRL2, FCRL5, FGF1, FGF7, FGFR2, FGFR3, FGFR4, FHL2, FIBCD1, FILIP1L, FLRT3, FLT1, FLT3, FLT4, FMOD, FOLR2, FRAS1, FRMD6, FRZB, FST, FUT7, FZD4, GAS6, GBP5, GHR, GIMAP4, GIMAP8, GJC1, GLT8D2, GPR39, GPRC5B, GRK5, GUCY1A2, GXYLT2, GZMA, GZMB, HBEGF, HCK, HDAC9, HES4, HES6, HEYL, HMCN1, HMOX1, HOXD9, HSPA6, HSPD1, HTRA1, ICAM2, IDO1, IFI44L, IGF1, IGSF3, IL1R1, IL2RB, IL33, IL34, IL3RA, IL6R, IL7R, INHBA, INPP4B, IRF4, IRX1, ITGA2, ITGA5, ITGA7, ITGAL, ITGB7, ITK, JAG1, JAM3, KAT2B, KCNA3, KCNH6, KCNJ8, KCNK1, KCNK15, KCNK17, KCNMB1, KCNN4, KCTD5, KDR, KIAA0408, KIF18B, KLF12, KLK6, KLRK1, KMO, LAD1, LAMB3, LAMC1, LAMC2, LAMP3, LBH, LCK, LCNL1, LGMN, LILRA4, LILRB5, LIPA, LMX1B, LOX, LOXL1, LOXL2, LPAR1, LPAR6, LRATD2, LRG1, LRP1, LRRK2, LTBP2, LTK, LUM, LY96, MACC1, MAFB, MAP1A, MAP1B, MAP2, MAP3K14, MAP3K20, MAPK11, MAPK15, MAPT, MDFIC, MEIS1, MERTK, MET, MFAP5, MLF1, MLXIPL, MME, MMP11, MMP14, MMRN2, MNDA, MNX1, MRC1, MRC2, MS4A1, MS4A6A, MSR1, MX1, MYCL, MYH11, MYLK, MYO1B, MYO1E, NBL1, NCF2, NECTIN1, NECTIN2, NGFR, NIBAN3, NINJ1, NKX3-1, NLRP1, NLRP2, NLRP3, NOSTRIN, NOTCH3, NPC1, NPR1, NPY1R, NR5A2, NRN1, NTN4, NTRK2, OAS1, OLFML3, OLR1, OPRD1, ORAI2, OVOL2, OXTR, P2RX5, P2RY13, PALMD, PAMR1, PARM1, PAX5, PBK, PCDH17, PCDH18, PDE10A, PDE5A, PDGFA, PDGFB, PDGFD, PDGFRA, PDGFRB, PDGFRL, PDK1, PDLIM4, PDPN, PDZK1, PGF, PHEX, PHLDB2, PIEZO2, PILRA, PITX1, PLA2G7, PLA2R1, PLCE1, PLEKHH2, PLTP, PLVAP, PLXDC1, PMAIP1, PODXL, POTEI, POU2AF1, POU2F2, PPARG, PRCP, PRDM1, PRICKLE1, PRKAA2, PRLR, PRODH, PROX1, PRRX1, PSD3, PTGFRN, PTGIS, PTHLH, PTK7, PTP4A3, PTPRB, PTPRG, PTPRM, RAPGEF3, RASD1, RASGRP1, RASSF4, RBMS3, RBPMS, RCAN2, RERG, RGL3, RGS5, RIPK4, ROBO4, ROR2, RUBCN, RUNX1T1, RUNX2, S100A1, S1PR3, SAMD5, SAPCD2, SASH1, SCGB1D2, SCIMP, SCN9A, SDC2, SDC4, SELENBP1, SELP, SELPLG, SEMA4B, SEPTIN4, SEPTIN5, SERPINB5, SERPINE1, SFRP4, SH3RF2, SHANK3, SHD, SIGLEC1, SIM2, SIRPA, SIX1, SLAMF1, SLAMF8, SLC11A1, SLC16A1, SLC1A3, SLC2A1, SLC2A9, SLC35G2, SLC6A1, SLC7A11, SLC7A5, SLC7A7, SLC9A3R2, SLCO2A1, SLIT2, SLIT3, SMOC2, SMPD3, SMTN, SNCA, SNX10, SOD3, SORCS1, SOSTDC1, SOX10, SOX17, SOX18, SOX5, SP140, SPA17, SPAG17, SPATA18, SPIB, SPOCK1, SPON1, SPON2, SRPX, ST6GAL1, STAT4, STEAP1, STEAP4, SULF1, SULF2, SVEP1, SYNPO2, SYVN1, TACC2, TBX2, TBX3, TCF7, TCIM, TCL1A, TEK, TENT5C, TFAP2B, TGFB3, TGM2, THBD, THBS2, THEMIS, THY1, TIMP2, TLK2, TLR1, TLR2, TLR4, TLR6, TLR7, TLR9, TMEM119, TMEM45B, TMEM97, TMEM98, TNC, TNFRSF13B, TNFRSF13C, TNNT2, TNS4, TNXB, TP63, TPPP3, TRAC, TRAF1, TRBC1, TREM2, TRIB3, TRIM29, TRPC6, TSPAN13, TSPAN18, TUBB2A, TWIST1, UNC5A, VCAM1, VCL, VEGFC, VSIG4, VSNL1, VSTM2L, WFS1, WWC1, ZAP70, ZBTB33, ZEB1, ZFAT, ZNF296

myoepi  |  positive: TRIM29, IRX1, TNS4, SOSTDC1, FAT2, COL17A1, LAMB3, SAMD5, TFAP2B, SOX10, AC007906.2, CALML3, DSG3, ECRG4, CDH3, FST, F3, TP63, MME, VSNL1, KLK6, DSC3, SERPINB5, KLK10, NTRK2, BRINP1, MAOB, MYH11, SFRP1, ANXA8, SH3RF2, CX3CL1, TNC, ADORA2B, CNN1, PROM1, GABRP, CDH13, PDLIM4, GRP, ERRFI1, LCN2, CRB2, PAMR1, LAMC2, MYLK, IL34, ANXA3, NGFR, PGR, VTCN1, FGF1, DST, CSPG4, KCNMB1, TUBB2B, LTBP2, COL7A1, SYNM, FGFR2, MET, CES1, ITGA2, RNF39, CXCL2, CADM3, HOXA5, EGFR, ADTRP, ATL1, CCL28, KCNN4, SDK2, ADAMTS5, KLHL13, DMD, DKK3, CCN3, OXTR, CMYA5, NAV2, TMEM98, ABCC3, NTN4, GRAMD2B, LGR4, KIT, FGF2, CDH23, SNCA, TLR2, COL4A5, DMKN, RNF180, LURAP1, TGFB2, ETV1, PALMD, JAG1, GRIK3, ALDH7A1, PDGFA, ITGA3, FHL2, CARD10, SOX9, CXADR, WNT4, S100A1, RIPK4, BARX2, WWC1
myoepi  |  negative: ABCA8, ABCC12, ABCC8, ABCC9, ABI3BP, ACVRL1, ADA, ADAM33, ADAMDEC1, ADAMTS12, ADAMTS14, ADAMTS4, ADCYAP1, ADGRL4, ADORA3, ADRA1B, ADRA2A, AIM2, ALPL, ANGPT1, APLNR, ASPN, AVPR1A, BANK1, BCL2A1, BCL6B, BDKRB2, BLK, BMP2, BMP6, BNC2, BPIFB1, C4B, C7, CALCRL, CCDC102B, CCDC141, CCL14, CCL19, CCN4, CCR1, CCR4, CCR6, CD19, CD1C, CD1D, CD1E, CD209, CD22, CD226, CD27, CD274, CD28, CD33, CD34, CD38, CD3G, CD40LG, CD5, CD7, CD72, CD79A, CD79B, CD86, CDH6, CFC1, CLDN5, CLEC10A, CLEC12A, CLEC14A, CLEC4C, CLEC5A, CMA1, CMKLR1, CNR2, CNRIP1, CNTNAP1, COCH, COL10A1, COL4A3, COL4A4, COLEC12, CR1, CST7, CTLA4, CTSG, CUX2, CXCR3, CXCR6, CYSLTR1, DCLK1, DERL3, DLK1, DLL4, DLX4, DNASE1L3, DPT, DPY19L2, EBF2, ENPP2, ENPP3, EPHB1, EREG, ESM1, F2RL3, FAM107A, FAT4, FCER1A, FCGR2B, FCRL1, FCRL2, FCRL3, FCRL5, FCRLA, FGF7, FGR, FLT1, FLT4, FOLH1, FOLR2, FOXD2, FPR1, FREM1, FUT7, GABRD, GIMAP5, GIMAP8, GJC1, GLI2, GPR174, GPR4, GRIK4, GRM4, GUCY1A2, GZMA, GZMB, GZMK, HAS2, HBEGF, HCK, HDC, HEYL, HGF, HLX, HOXD10, HOXD9, HPGD, HPGDS, HPSE, HPSE2, HS3ST3A1, ICAM2, ICOS, IDO1, IGF1, IL12RB1, IL18R1, IL1RL1, IL21R, IL3RA, IRF4, KCNA3, KCNF1, KCNH6, KCNJ8, KCNK10, KCNN3, KDR, KLRD1, KLRG1, LAMP3, LCNL1, LDB3, LILRA4, LILRB2, LILRB4, LILRB5, LPL, LRRC15, LTK, LY96, MAP1A, MAP3K7CL, MECOM, MFAP5, MLXIPL, MNDA, MRC1, MS4A1, MS4A2, MTNR1B, NCF1, NIBAN3, NLRP3, NOD2, NOS3, NPNT, NPR1, NR4A3, NR5A2, NRN1, NRROS, OLR1, OPRD1, P2RX1, P2RX5, P2RX7, P2RY13, P2RY14, PAX5, PCDH17, PCDH18, PCDH8, PDE10A, PDE2A, PDE3A, PHEX, PHF21B, PI16, PIEZO2, PILRA, POTEI, POTEJ, POU2AF1, PPFIA3, PPP1R1C, PRDM6, PRF1, PTGDR, PTGIR, PTGIS, PTGS1, PTPRB, RCAN2, RECK, RELN, RGS5, ROBO4, RUNX1T1, S1PR1, SCIMP, SCN9A, SELP, SEMA7A, SFRP4, SH2D1A, SH2D2A, SHANK3, SHD, SIGLEC1, SIGLEC6, SIGLEC9, SLAMF6, SLC11A1, SLC18A2, SLC1A3, SLC35G2, SLC6A1, SLIT2, SMPD3, SOD3, SOX17, SPIB, SPOCK1, SPON1, STAP1, TAL1, TBX2, TBXA2R, TCL1A, TDO2, TEK, TH, TIE1, TIGIT, TLR7, TLR9, TMEM119, TMEM255B, TNFRSF13B, TNFRSF13C, TNFRSF17, TNFSF8, TPSG1, TREML2, TRH, TSPAN18, TSPAN7, TWIST1, TWIST2, UBASH3A, VSIG4, WFDC1, WNT2, ZBP1, ZFHX4, ZNF683

perivas  |  positive: GUCY1A2, CDH6, EBF2, AVPR1A, TRPC6, GJC1, TBX2, RGS5, ABCC9, ITGA7, ADRA1B, HEYL, SLC6A1, CSPG4, TPPP3, KCNJ8, SEPTIN4, CCDC102B, PDGFRB, NOTCH3, COL18A1, EDNRA, C1QTNF1, PGF, AFAP1L2, JAG1, SDC2, MYO1B, SYNPO2, PDGFA, FILIP1L, PDE5A, SOX5, LDB3, ANGPT1, DPY19L2, ADAMTS4, TDO2, PDE3A, CD248, ADAMTS12, RCAN2, RBMS3, SOD3, THY1, PRRX1, BGN, SLIT3, STEAP4, EPAS1, DKK3, MYLK, FRZB, PCDH18, SEPTIN5, ADAMTS1, COL5A2, CNTNAP1, ADAMTS2, VCL, MAP2, COL5A1, PHLDB2, PLXDC1, NPNT, ADAMTS14, S1PR3, FLT1, SMOC2, AEBP1, LBH, PAMR1, MYH11, SPRY2, TIMP3, PPARG, LOXL2, RBPMS, MAP3K20, SPON2, PTPRG, PTP4A3, MAP3K7CL, CCN2, GRK5, CCND2, WFDC1, ADRA2A, DMD, CAVIN3, PIEZO2, EPHA2, MAP1B, PLVAP, PLCE1, SPECC1, SULF1, FZD4, CALCRL, LAMC1, AQP1, DDR2, HES4, PARM1, FOXC2, LPL, NRP1, KANK2, SERPINH1, RERG, TRO, ENG, PALLD, SMTN, MYO1E, TNC, MAF, RFTN1, FGF7, KLF12
perivas  |  negative: ABCA10, ABCC12, ABCC8, ABI3BP, AC007906.2, ADAM28, ADAMDEC1, ADCYAP1, ADGRE2, ADORA2B, ADORA3, ADRB2, ADTRP, AGR3, AIF1, AIM2, ALPK3, ANXA8, BAALC, BANK1, BARX2, BCL11B, BCL2A1, BCL6B, BDKRB2, BLK, BLNK, BMP6, BMP8B, BMPR1B, BNC2, BPIFB1, BRINP1, C3AR1, C4B, CACNA1B, CACNG4, CADM3, CALML3, CARD10, CARD14, CARD9, CASP10, CCL22, CCL28, CCR1, CCR4, CCR6, CD19, CD1C, CD1D, CD1E, CD209, CD22, CD27, CD274, CD33, CD38, CD7, CD72, CD79A, CD79B, CD83, CD86, CD8A, CDH13, CDH23, CDKN2B, CEBPA, CELSR3, CENPA, CES1, CGNL1, CHAD, CHST8, CLEC10A, CLEC12A, CLEC4C, CLEC5A, CLEC7A, CLGN, CLIC3, CMA1, CMKLR1, CMYA5, CNR2, COL10A1, COL17A1, COL4A3, COL4A4, COLEC12, COMP, CPVL, CPXM2, CR1, CRB2, CSF2RA, CSF3R, CST7, CTNND2, CTSG, CTSW, CUX2, CX3CL1, CXADR, CXCL2, CXCR3, CXCR6, CYP27A1, CYSLTR1, DERL3, DLK1, DLL4, DLX4, DNAH5, DNASE1L3, DPT, DSC3, DSG3, ECRG4, EDN1, EFR3B, EGR2, ENPP3, ENTPD2, EPB41L3, EPHB1, EREG, ERG, F2RL3, F7, FAT2, FAT4, FBLN5, FCER1A, FCGR2A, FCGR2B, FCGR3A, FCMR, FCRL1, FCRL2, FCRL3, FCRL5, FCRLA, FGFR4, FGR, FLRT3, FLT3, FLT4, FOLH1, FOLR2, FPR1, FREM1, FST, FUT7, GABRP, GBP5, GIMAP5, GPR174, GPR34, GRIK4, GRM4, GRP, GZMA, GZMB, GZMK, HAVCR2, HBEGF, HCK, HDC, HMCN1, HMOX1, HOXD10, HOXD9, HPGD, HPGDS, HPSE, HPSE2, HS3ST3A1, HSPA6, IDO1, IGF1, IL18, IL18R1, IL1RL1, IL33, IL3RA, IL6R, IRF4, IRS2, IRX1, ITK, JAM2, KCNF1, KCNH6, KCNK10, KCNK15, KCNMA1, KCNN3, KCNN4, KIAA0408, KIT, KLHL13, KLK10, KLK6, KLRD1, KLRG1, KMO, LAMP3, LCK, LCN2, LCNL1, LIF, LILRA4, LILRB2, LILRB4, LILRB5, LOX, LPAR1, LRG1, LRRC15, LRRK2, LTK, LY75, MAOB, MAP1A, MAPK11, ME1, MET, MFAP5, MLXIPL, MME, MNDA, MNX1, MRC1, MS4A1, MS4A2, MSR1, MTNR1B, MUC16, MUC5B, MYCL, NCF1, NCF2, NIBAN3, NKX3-1, NLRP3, NOD2, NOS1AP, NOS3, NPR1, NR4A3, NR5A2, NRROS, OLR1, OPRD1, OXTR, P2RX1, P2RX5, P2RX7, P2RY13, P2RY2, P2RY6, PAX5, PCDH10, PCDH17, PCDH8, PDE2A, PDGFD, PDGFRA, PDGFRL, PDLIM4, PDPN, PDZK1, PHEX, PHF21B, PI16, PILRA, PLA2G7, PLAUR, PLCG2, PLXNC1, PMAIP1, POTEI, POTEJ, POU2AF1, POU2F2, PPFIA3, PPP1R1C, PRDM6, PRF1, PRICKLE1, PRKCQ, PRLR, PRODH, PROM1, PROX1, PTAFR, PTGES, PTGIS, PTGS1, RECK, RELN, RIMS3, RIPK4, RNF39, ROBO4, ROR2, RTN1, S100A1, SAMD5, SCGB1D2, SCIMP, SCN9A, SDK2, SELP, SEMA7A, SERPINA5, SERPINB5, SFRP1, SFRP4, SH2D1A, SH3RF2, SHANK3, SHD, SIGLEC1, SIGLEC6, SIGLEC9, SIM2, SIRPA, SLAMF6, SLAMF8, SLC11A1, SLC18A2, SLC1A3, SLC35G2, SLC7A11, SLC7A7, SLCO2A1, SMPD3, SNCA, SNX10, SORCS1, SOSTDC1, SOX10, SOX17, SPA17, SPAG17, SPIB, SPOCK1, SPON1, STAP1, STEAP1, TAL1, TCL1A, TEK, TFAP2B, TFPI2, TGM2, TIE1, TIGIT, TLR1, TLR2, TLR6, TLR7, TLR9, TMEM255B, TNFRSF13B, TNFRSF13C, TNFRSF17, TNFSF8, TNNT2, TNS4, TP63, TPSG1, TREM2, TRH, TRIM29, TRPM2, TSPAN18, TSPAN7, TUBB2B, TUBB3, TWIST2, VSIG4, VSNL1, VTCN1, WNT2, WNT4, ZBP1, ZFHX4, ZNF296, ZNF683

stromal  |  positive: POSTN, DCN, ADAM33, RUNX1T1, PDGFRA, LUM, FAP, PLEKHH2, THBS2, COL5A1, COL5A2, AEBP1, CCN2, CDH11, SVEP1, TMEM119, FGF7, FBLN2, DCLK1, CCDC80, CXCL12, ABI3BP, ADAMTS2, FBLN5, SRPX, CCN4, SPON1, HMCN1, MFAP5, PTGIS, SERPINE1, ELN, SLIT2, SPOCK1, INHBA, COL10A1, TWIST2, ADAMTS12, FREM1, ADAM12, RBMS3, CD248, LRP1, PDGFRB, PCDH18, PRRX1, ZFHX4, TWIST1, SPON2, MMP14, GLT8D2, BGN, ANGPTL2, HPSE2, ROR2, EMP1, LPAR1, THY1, CTSK, CTHRC1, ABCA10, WNT2, PDPN, LOX, TIMP2, VCAM1, IGF1, CPXM2, LRRC15, PRICKLE1, HAS2, ABCA8, CCN1, HTRA1, DNM1, PDGFRL, PLXDC1, ASPN, BNC2, MRC2, OLFML3, SOD3, PI16, F2R, SULF1, DPT, RELN, PDGFD, COLEC12, STEAP1, COL4A4, SFRP4, GLI2, FAT4, TIMP3, IL33, VGLL3, SLIT3, EDNRA, LOXL1, PTK7, PLAGL1, TNXB, HGF, MMP11, ALPL, SMOC2, RECK, COMP, GXYLT2, SUGCT, TRO, HSPG2, SDC2, LTBP2, EGFR, FRMD6, C7, FMOD, NBL1, FRZB, PTGFRN, CD34, CYP1B1, JAM3, MEIS1, ACVRL1, ANGPTL4, COL18A1, TMEM98, DAB2, FILIP1L, JAM2, CDKN2B, IL1R1, EGR2, DIO2, TRPC1, MEIS2, PLA2R1, TGFB3, CLSTN3
stromal  |  negative: ABCC12, ABI3, AC007906.2, ADAMDEC1, ADCYAP1, ADGRE2, ADGRL4, ADORA2B, ADORA3, ADRA1B, ADRB2, ADTRP, AIM2, ALPK3, ANGPT1, ANXA3, ANXA8, APLNR, AURKB, AVPR1A, BAALC, BANK1, BARX2, BCL11B, BCL2A1, BCL6B, BDKRB2, BLK, BMP2, BMP6, BMP8B, BMPR1B, BPIFB1, BRINP1, C4B, CACNG4, CADM3, CALB2, CALML3, CARD10, CARD14, CARD9, CASP10, CAVIN2, CCDC141, CCL14, CCL22, CCL28, CCN3, CCR1, CCR4, CCR5, CCR6, CD19, CD1C, CD1D, CD1E, CD209, CD22, CD226, CD247, CD27, CD274, CD28, CD33, CD38, CD3G, CD40LG, CD5, CD7, CD72, CD79A, CD79B, CD83, CD86, CD8A, CDH13, CDH5, CDH6, CELSR3, CENPA, CFC1, CHAD, CHST8, CLDN5, CLEC12A, CLEC14A, CLEC4C, CLEC5A, CLEC7A, CLGN, CLIC3, CMA1, CNR2, COCH, COL17A1, COL4A3, CR1, CRB2, CSF3R, CST7, CTLA4, CTSG, CTSW, CUX2, CXADR, CXCL2, CXCR3, CXCR6, CYSLTR1, DERL3, DLL4, DLX4, DMD, DNAH5, DNASE1L3, DPY19L2, DSC3, DSG3, EBF2, ECRG4, EDN1, EGF, ENPP3, ENTPD2, EPHB1, EREG, ERG, ESM1, F2RL3, FAM107A, FAT2, FCER1A, FCGR2B, FCMR, FCRL1, FCRL2, FCRL3, FCRL5, FCRLA, FGFR4, FGR, FLRT3, FLT1, FLT3, FLT4, FOLH1, FOXD2, FPR1, FUT7, GABRD, GABRP, GIMAP5, GIMAP8, GPR174, GPR39, GPR4, GRIK3, GRIK4, GRM4, GRP, GUCY1A2, GZMA, GZMB, GZMK, HAVCR2, HBEGF, HCK, HDAC9, HDC, HLX, HOXA5, HOXD10, HOXD9, HPGD, HPGDS, HPSE, HS3ST3A1, ICAM2, ICOS, IDO1, IL12RB1, IL18, IL18R1, IL1RL1, IL21R, IL2RB, IL3RA, IL6R, IRF4, IRX1, ITGA2B, ITGA7, ITK, KCNA3, KCNF1, KCNH6, KCNJ8, KCNK10, KCNK17, KCNMB1, KCNN3, KCNN4, KDR, KIF18B, KIT, KLHL13, KLK10, KLK6, KLRD1, KLRG1, KLRK1, KMO, LAMB3, LAMC2, LAMP3, LCK, LCN2, LCNL1, LDB3, LIF, LILRA4, LILRB2, LILRB4, LILRB5, LMO2, LRG1, LTK, MAOB, MAP2, MAP3K7CL, MECOM, MET, MLXIPL, MMRN2, MNDA, MNX1, MS4A1, MS4A2, MTNR1B, MUC16, MUC5B, MYCL, MYH11, NAV2, NCCRP1, NCF1, NEK2, NIBAN3, NKX3-1, NLRP2, NLRP3, NOD2, NOS1AP, NOS3, NOSTRIN, NPNT, NPR1, NPY1R, NR4A3, NR5A2, NRROS, NTRK2, OLR1, OPRD1, OXTR, P2RX1, P2RX5, P2RX7, P2RY13, P2RY14, P2RY2, P2RY6, PALMD, PAX5, PBK, PCDH10, PCDH17, PCDH8, PDE2A, PDE3A, PDZK1, PGR, PHEX, PHF21B, PILRA, PITX1, PLA2G7, PLCE1, PLCG2, PMAIP1, POTEI, POTEJ, POU2AF1, POU2F2, PPFIA3, PPP1R1C, PRF1, PRKAA2, PRKCQ, PROCR, PRODH, PROM1, PROX1, PTAFR, PTGDR, PTGS1, PTPRB, RASGRP1, RERG, RGS5, RIMS3, RIPK4, RNF39, ROBO4, RTN1, S100A1, S1PR1, SCGB1D2, SCIMP, SCN9A, SDK2, SELP, SERPINA5, SERPINB5, SH2D1A, SH2D2A, SH3RF2, SHANK3, SHD, SIGLEC1, SIGLEC6, SIGLEC9, SIM2, SLAMF1, SLAMF6, SLC11A1, SLC18A2, SLC1A3, SLC2A9, SLC6A1, SLC7A11, SLC7A5, SLCO2A1, SMPD3, SNCA, SOSTDC1, SOX10, SOX17, SOX18, SPA17, SPAG17, SPIB, STAP1, TAL1, TBXA2R, TCL1A, TDO2, TEK, TFAP2B, TH, THBD, TICRR, TIE1, TIGIT, TLR6, TLR7, TLR9, TMEM255B, TMEM45B, TNFRSF13B, TNFRSF13C, TNFRSF17, TNFSF8, TNS4, TP63, TPPP3, TPSG1, TREM2, TREML2, TRH, TRIM29, TRPC6, TSPAN7, TUBA4A, TUBB2B, UBASH3A, UNC5A, VSNL1, VSTM2L, VTCN1, WNT5B, ZBP1, ZFAT, ZNF296, ZNF683

tumor  |  positive: FIBCD1, CLGN, TMEM97, MLXIPL, CDH2, ECM1, ITPRID2, SELENBP1, ITGA2B, NRAS, CYP24A1, CCDC170, DLK1, CACNA1B, MTNR1B, MUC1, PPM1D, FASN, ACE, GRIK4, IKZF2, RNF43, BRIP1, FGFR3, SPAG17, PBK, ABCC8, POTEJ, ABCC12, PBX1, PSMC5, UGDH, SLC12A2, TH, WDR90, INPP4B, SREBF1, ERBB3, PLXNB1, GREB1L, AR, POTEF, TLK2, PHF21B, ADGRB2, UNC5A, PPP1R1C, PCDH8, TNNT2, KCNH6, MLF1, SORCS1, KIF18B, TUBD1, CCND1, RGL3, HSF4, FADS2, SLC2A4RG, F7, MAP7, SIX1, EXO1, TONSL, SLC30A8, GSR, ITGB6, RRM2, GSE1, OVOL2, MAPK15, FGFR4, GRM4, MYCN, LMX1B, TCIM, CENPA, HES1, ANO1, GPR39, TICRR, EHF, TBC1D30, MST1R, TBX3, TCEA3, PRDM6, CHST8, HS6ST2, TC2N, PPM1H, RECQL4, TYMS, FBP1, CARD14, SLC25A39, PHGDH, FRAS1, KMO, NEK2, BMI1, AURKB, HSPD1, DNAH5, RET, MNX1
tumor  |  negative: ABCA10, ABCA8, ABCC9, ABI3, ABI3BP, AC007906.2, ACSL4, ACVRL1, ADA, ADAM12, ADAM33, ADAMDEC1, ADAMTS1, ADAMTS12, ADAMTS14, ADAMTS2, ADAMTS4, ADAMTS5, ADCYAP1, ADGRE2, ADGRL4, ADORA2B, ADORA3, ADRA1B, ADRA2A, ADTRP, AFAP1L2, AIF1, AIM2, ALPL, ANGPT1, ANGPTL2, ANGPTL4, ANXA3, ANXA8, AOAH, APLNR, APOBEC3G, ASPN, ATL1, AVPR1A, BAALC, BANK1, BCL11B, BCL2A1, BCL6B, BDKRB2, BLK, BMP2, BMP6, BMPR1B, BNC2, BPIFB1, BRINP1, C1QTNF1, C3AR1, C4B, C7, CACNG4, CADM3, CALB2, CALCRL, CALML3, CASP1, CASP10, CAVIN2, CAVIN3, CCDC102B, CCDC141, CCL14, CCL19, CCL22, CCN3, CCN4, CCND2, CCR1, CCR4, CCR5, CCR6, CD19, CD1C, CD1D, CD1E, CD209, CD22, CD226, CD247, CD248, CD27, CD274, CD28, CD302, CD33, CD34, CD38, CD3D, CD3E, CD3G, CD40, CD40LG, CD5, CD7, CD72, CD79A, CD79B, CD86, CD8A, CDH11, CDH13, CDH23, CDH5, CDH6, CEP112, CES1, CLDN5, CLEC10A, CLEC12A, CLEC14A, CLEC4C, CLEC5A, CLEC7A, CLIC3, CMA1, CMKLR1, CNN1, CNR2, CNRIP1, CNTNAP1, COCH, COL10A1, COL17A1, COL4A3, COL4A4, COLEC12, COMP, CPVL, CPXM2, CR1, CRB2, CSF1, CSF2RA, CSF3R, CSPG4, CST7, CTLA4, CTSG, CTSW, CUX2, CX3CL1, CXCL2, CXCR3, CXCR6, CYP27A1, CYSLTR1, DCLK1, DERL3, DKK3, DLL4, DLX4, DMD, DNASE1L3, DPT, DPY19L2, DSC3, DSG3, EBF2, ECRG4, EDN1, EDNRA, ELN, ENPP2, ENPP3, EPHB1, EREG, ESM1, ETV1, F13A1, F2RL3, F3, FAM107A, FAP, FAT2, FAT4, FBLN2, FBLN5, FCER1A, FCGR2A, FCGR2B, FCGR3A, FCMR, FCRL1, FCRL2, FCRL3, FCRL5, FCRLA, FES, FGF1, FGF2, FGF7, FGR, FILIP1L, FLT1, FLT3LG, FLT4, FMOD, FOLH1, FOLR2, FOXC2, FPR1, FREM1, FRZB, FSCN1, FST, FUT7, FYN, GABRD, GABRP, GAS6, GBP5, GIMAP5, GIMAP8, GJC1, GLI2, GLT8D2, GPR174, GPR34, GPR4, GRIK3, GRK5, GRP, GUCY1A2, GZMA, GZMB, GZMK, HAS2, HAVCR2, HCK, HDC, HEYL, HGF, HLX, HMCN1, HOXA5, HOXD10, HOXD9, HPGD, HPGDS, HPSE, HPSE2, HS3ST3A1, HSPA6, ICAM2, IDO1, IFI44L, IGF1, IL12RB1, IL18, IL18R1, IL1RL1, IL21R, IL2RB, IL33, IL34, IL3RA, IL6R, INHBA, IRF4, IRS2, IRX1, ITGA7, ITGAL, ITIH5, ITK, JAG1, JAK3, JAM2, JAM3, KCNA3, KCNF1, KCNJ8, KCNK10, KCNK17, KCNMB1, KCNN3, KCNN4, KDR, KIAA0408, KIT, KLHL13, KLK10, KLK6, KLRD1, KLRG1, KLRK1, LAIR1, LAMP3, LBH, LCK, LCN2, LCNL1, LDB3, LIF, LILRA4, LILRB2, LILRB4, LILRB5, LOX, LPAR1, LPL, LRRC15, LRRK2, LTK, LURAP1, LY75, LY96, MAOB, MAP1A, MAP3K7CL, MERTK, MFAP5, MME, MMP9, MMRN2, MNDA, MPP1, MRC1, MS4A1, MS4A2, MSR1, MUC16, MYH11, NAV2, NCCRP1, NCF1, NCF2, NGFR, NIBAN3, NLRP1, NLRP3, NOD2, NOS3, NOSTRIN, NPNT, NPR1, NR4A3, NR5A2, NRN1, NRP2, NRROS, NTRK2, OLFML3, OLR1, OPRD1, P2RX1, P2RX5, P2RX7, P2RY13, P2RY14, P2RY6, PALMD, PAMR1, PARP15, PAX5, PCDH10, PCDH17, PCDH18, PDE2A, PDE3A, PDGFD, PDGFRA, PDGFRB, PDPN, PDZK1, PGF, PGR, PHEX, PI16, PIEZO2, PLA2G7, PLAGL1, PLCE1, PLCG2, PLEKHH2, PLXDC1, POU2AF1, POU2F2, PPFIA3, PRF1, PRICKLE1, PRKCQ, PROCR, PROM1, PROX1, PRRX1, PTAFR, PTGDR, PTGIR, PTGIS, PTGS1, PTPRB, PTPRM, RASGRP1, RASSF2, RASSF4, RBMS3, RCAN2, RECK, RELN, RGS5, RIMS3, RNF180, RNF39, ROBO4, ROR2, RTN1, RUNX1T1, RUNX2, S1PR1, SAMD5, SASH1, SATB1, SCGB1D2, SCIMP, SCN9A, SDC2, SDK2, SELP, SELPLG, SEMA7A, SEPTIN4, SERPINB5, SFRP1, SFRP4, SH2D1A, SHANK3, SHD, SIGLEC1, SIGLEC6, SIGLEC9, SLAMF1, SLAMF6, SLAMF8, SLC11A1, SLC18A2, SLC1A3, SLC35G2, SLC6A1, SLC7A11, SLC7A7, SLCO2A1, SLCO2B1, SLIT2, SLIT3, SMPD3, SNCA, SOD3, SOSTDC1, SOX10, SOX17, SOX5, SP140, SPIB, SPOCK1, SPON1, SPRY2, SRPX, ST6GAL1, STAP1, STAT4, SUGCT, SULF1, SVEP1, SYNPO2, TAL1, TBX2, TBXA2R, TCF7, TCL1A, TDO2, TEK, TFAP2B, TGFB2, TGFBR3, TGM2, THBD, THEMIS, THY1, TIE1, TIGIT, TLR1, TLR2, TLR4, TLR6, TLR7, TLR9, TMEM119, TMEM255B, TNFRSF13B, TNFRSF17, TNFSF8, TNS4, TP63, TPPP3, TPSG1, TRBC1, TREM2, TREML2, TRH, TRIM29, TRO, TRPC1, TRPC6, TSPAN18, TSPAN7, TUBB2B, TUBB6, TWIST1, TWIST2, UBASH3A, VCAM1, VGLL3, VSIG4, VSIR, VSNL1, VTCN1, WFDC1, WNT2, WNT4, WNT5B, ZAP70, ZBP1, ZEB1, ZFHX4, ZNF683

Below, we show the number of negative markers that overlap with the positive markers of each cell type. To reliably estimate contamination, each cell type should share at least ~5 negative markers with the positive marker set of every other cell type. If these overlaps are too small, contamination estimates become unstable.

In such cases, the marker definition can be relaxed by adjusting the thresholds used in markers_from_reference above.

[47]:
ctypes = list(markers["xenium"].keys())
overlap_df = pd.DataFrame(0, index=ctypes, columns=ctypes, dtype=int)

for c in ctypes:
    neg_c = set(markers["xenium"][c].get("negative", []))
    for d in ctypes:
        pos_d = set(markers["xenium"][d].get("positive", []))
        overlap_df.loc[d, c] = len(neg_c & pos_d)

overlap_df
[47]:
B DCIS1 DCIS2 T dendritic endo macro mast myoepi perivas stromal tumor
B 0 50 48 41 26 44 29 47 41 49 48 57
DCIS1 49 0 20 75 50 37 48 50 12 41 37 23
DCIS2 39 16 0 78 41 27 40 56 5 29 25 12
T 26 48 52 0 27 37 24 43 33 28 47 66
dendritic 50 66 63 73 0 49 42 70 48 53 56 70
endo 108 90 92 110 101 0 94 86 63 45 66 95
macro 71 76 74 102 49 60 0 74 48 79 54 88
mast 28 27 29 37 24 24 20 0 20 28 27 30
myoepi 97 33 58 98 85 55 85 70 0 59 61 75
perivas 87 49 60 109 70 6 80 92 34 0 24 71
stromal 98 70 84 118 88 31 94 109 42 40 0 90
tumor 22 16 6 66 29 22 27 38 14 26 28 0

Marker purity

To quantify how well each segmented cell in the spatial transcriptomics data matches its annotated cell type, we defined a marker-based purity (marker_balanced_accuracy) score that jointly evaluates the expression of positive (positive_marker_recall) and the absence of neighborhood-associated negative markers (negative_marker_avoidance). The method accounts for the spatial context of each cell and is motivated by the assumption that differences between scRNA-seq and spatial transcriptomics-derived cell type profiles arise mainly from local contamination by neighboring cells.

[48]:
for method, st in st_dict.items():
    purity = st.sp.marker_purity(
        cell_type_key="transferred_cell_type",
        markers=markers[method],
        inplace=True,
    )
INFO     Creating graph using `None` transform and `1` libraries.
INFO     Creating graph using `None` transform and `1` libraries.
INFO     Creating graph using `None` transform and `1` libraries.

Proseg exhibits the highest marker_balanced_accuracy across most cell types, with the exception of DCIS1. To better interpret the overall purity scores, it is informative to examine the positive_marker_recall and negative_marker_avoidance scores separately, as they capture distinct aspects of marker specificity.

[49]:
boxplot_per_celltype(st_dict, "marker_balanced_accuracy")
../_images/notebooks_10x_xenium_focus_94_0.png

The negative_marker_avoidance score quantifies the extent to which a focal cell expresses positive markers of neighboring cell types, and thus reflects contamination. Overall, contamination levels are low; however, increased contamination is observed for DCIS1.

[50]:
def box_strip_plot_per_celltype(
    st_dict,
    feature,
    celltype_col="transferred_cell_type",
    q=1.0,
    figsize=(15, 5),
    palette="Set2",
):
    dfs = []
    for method, st in st_dict.items():
        obs = st.sdata.tables["table"].obs
        tmp = obs[[celltype_col, feature]].copy()
        tmp = tmp[tmp[celltype_col].notna()]
        tmp["method"] = method
        dfs.append(tmp)

    df = pd.concat(dfs, ignore_index=True).dropna(subset=[celltype_col, feature])

    if q < 1:
        df = df[df[feature] <= df[feature].quantile(q)]

    fig, ax = plt.subplots(figsize=figsize)

    sns.boxplot(
        data=df,
        x=celltype_col,
        y=feature,
        hue="method",
        showcaps=True,
        showfliers=False,
        palette=palette,
        ax=ax,
    )

    sns.stripplot(
        data=df,
        x=celltype_col,
        y=feature,
        hue="method",
        dodge=True,
        jitter=0.2,
        alpha=0.6,
        linewidth=0,
        palette=palette,
        ax=ax,
    )

    # avoid duplicate legends
    handles, labels = ax.get_legend_handles_labels()
    ax.legend(handles, labels, title="method")

    ax.tick_params(axis="x", rotation=45)
    ax.set_xlabel("Cell type")
    ax.set_ylabel(feature)
    ax.legend(title="method", bbox_to_anchor=(1.02, 1), loc="upper left")
    fig.tight_layout()
    plt.show()
[51]:
box_strip_plot_per_celltype(st_dict, "negative_marker_avoidance")
../_images/notebooks_10x_xenium_focus_97_0.png

In contrast, the positive_marker_recall score measures how consistently a cell expresses markers expected for its assigned cell type.

[52]:
boxplot_per_celltype(st_dict, "positive_marker_recall")
../_images/notebooks_10x_xenium_focus_99_0.png

These metrics are most informative when considered jointly. For example, we plot the mean marker_balanced_accuracy (a supervised measure of both how well positive markers are captured and negative markers are avoided) against the mean border_admixture_score, an unsupervised contamination metric. In this combined view, we can see that Proseg shows slighltly better performance than the other two methods.

[53]:
rows = []
for _method, st in st_dict.items():
    corr_ncv_vs_center = st.sdata.tables["table"].obs["border_admixture_score"].median()
    negative_F1 = st.sdata.tables["table"].obs["marker_balanced_accuracy"].median()

    rows.append(
        {
            "Method": _method,
            "Border_admixture_score": corr_ncv_vs_center,
            "marker_balanced_accuracy": negative_F1,
        }
    )

results_df = pd.DataFrame(rows)

fig, ax = plt.subplots(figsize=(4, 3))

sns.scatterplot(
    data=results_df,
    x="Border_admixture_score",
    y="marker_balanced_accuracy",
    hue="Method",
    style="Method",
    s=100,
    palette="Set2",
    ax=ax,
)

ax.set_xlabel("Border_admixture_score (↓)")
ax.set_ylabel("marker_balanced_accuracy (↑)")
ax.set_title("Border_admixture_score vs. marker_balanced_accuracy")

legend = ax.legend(title="Method", frameon=False, bbox_to_anchor=(1.05, 1), loc="upper left")

fig.tight_layout()
plt.show()
../_images/notebooks_10x_xenium_focus_101_0.png

Neighborhood contamination

Marker purity summarizes how well a cell matches its own markers and avoids neighborhood-relevant negatives. In many cases, we also want to quantify

  1. how many contaminating transcripts are present per cell and

  2. which neighboring cell types contribute to this signal. We therefore compute neighborhood contamination.

[54]:
for method, st in st_dict.items():
    per_cell_df, _, cont_bin, _ = st.sp.neighbor_contamination(
        cell_type_key="transferred_cell_type", markers=markers[method]
    )

Among the evaluated methods, Proseg yields the smallest number of cells showing detectable contamination.

[55]:
methods_df = []

for method, st in st_dict.items():
    obs = st.sdata.tables["table"].obs
    counts = obs.loc[obs["contamination_counts"] > 0, "transferred_cell_type"].value_counts().rename(method)
    methods_df.append(counts)

merged = pd.concat(methods_df, axis=1)
merged
[55]:
xenium bidcell proseg
transferred_cell_type
T 28 37 10
DCIS1 11 6 7
DCIS2 9 4 6
B 8 4 2
macro 8 13 10
dendritic 6 5 4
stromal 6 6 7
endo 2 6 2
mast 1 2 7
myoepi 1 2 3
perivas 1 2 0
tumor 0 0 0

The spatial plot below shows the number of contaminating transcripts in each cell.

[56]:
def plot_feature_labels(sdata, method, feature, axes, i):
    labels = sdata.tables["table"].obs["transferred_celltype_plot"].unique().astype(str).tolist()
    cols = [col_celltype[lab] for lab in labels]

    sdata.tables["table"].obs["region"] = "cell_boundaries"
    sdata.set_table_annotates_spatialelement("table", region="cell_boundaries")

    sdata.pl.render_shapes(
        "cell_boundaries",
        color="transferred_celltype_plot",
        palette=cols,
        groups=labels,
        outline_color="white",
        outline_width=0.5,
    ).pl.show(ax=axes[0, i], title=f"{method}:Cell boundaries colored by cell type", coordinate_systems="global")

    sdata.pl.render_shapes(
        "cell_boundaries",
        color=feature,
        outline_color="white",
        outline_width=0.5,
    ).pl.show(ax=axes[1, i], title=f"{method}: {feature}", coordinate_systems="global")
[57]:
fig, axes = plt.subplots(2, 3, figsize=(15, 10), constrained_layout=True)
axes = np.atleast_2d(axes)
for i, (method, st) in enumerate(st_dict.items()):
    plot_feature_labels(st.sdata, method, "contamination_counts", axes, i)
WARNING  render_shapes: Found 155 NaN values in color data. These observations will be colored with the 'na_color'.
WARNING  render_shapes: Found 181 NaN values in color data. These observations will be colored with the 'na_color'.
WARNING  render_shapes: Found 236 NaN values in color data. These observations will be colored with the 'na_color'.
../_images/notebooks_10x_xenium_focus_108_1.png

The heatmap below summarizes contamination strength for each source–target cell-type pair. Each entry represents the mean contamination strength across all evaluable target cells of the given target cell type, where the source-specific contamination strength is computed as the fraction of transcripts in the target cell that correspond to contamination-relevant markers of the source cell type.

The bubble plot illustrates the contamination strength (bubble color) and the number of evaluable target cells (bubble size).

[58]:
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from matplotlib.colors import Normalize

vmax = max(st.sdata.tables["table"].uns["contamination_strength_matrix"].max().max() for st in st_dict.values())
norm = Normalize(vmin=0, vmax=vmax)

cell_type_order = list(next(iter(st_dict.values())).sdata.tables["table"].uns["contamination_strength_matrix"].columns)

fig, axes = plt.subplots(1, 3, figsize=(15, 5), constrained_layout=True)

for i, (method, st) in enumerate(st_dict.items()):
    cont_strength = st.sdata.tables["table"].uns["contamination_strength_matrix"]
    cont_n = st.sdata.tables["table"].uns["contamination_evaluable_cells_matrix"]

    # Force same source/target order and include missing rows/columns.
    cont_strength = cont_strength.reindex(
        index=cell_type_order,
        columns=cell_type_order,
    )
    cont_n = cont_n.reindex(
        index=cell_type_order,
        columns=cell_type_order,
    )

    plot_df = (
        cont_strength.stack(dropna=False)
        .rename("contamination_strength")
        .reset_index()
        .rename(columns={"level_0": "source", "level_1": "target"})
    )

    plot_df["n_evaluable"] = cont_n.stack(dropna=False).values
    plot_df = plot_df.dropna(subset=["n_evaluable"])

    plot_df["target"] = pd.Categorical(
        plot_df["target"],
        categories=cell_type_order,
        ordered=True,
    )
    plot_df["source"] = pd.Categorical(
        plot_df["source"],
        categories=cell_type_order,
        ordered=True,
    )

    sns.scatterplot(
        data=plot_df,
        x="target",
        y="source",
        size="n_evaluable",
        hue="contamination_strength",
        sizes=(20, 600),
        palette="Reds",
        hue_norm=norm,
        edgecolor="black",
        legend=False,
        ax=axes[i],
    )

    axes[i].set_title(method, fontsize=12)
    axes[i].set_xlabel("Target Cell Type")
    axes[i].set_xlim(-0.5, len(cell_type_order) - 0.5)
    axes[i].set_ylim(len(cell_type_order) - 0.5, -0.5)

    axes[i].set_xticks(range(len(cell_type_order)))
    axes[i].set_xticklabels(cell_type_order, rotation=45, ha="right")

    axes[i].set_yticks(range(len(cell_type_order)))
    if i == 0:
        axes[i].set_ylabel("Source Cell Type")
        axes[i].set_yticklabels(cell_type_order)
    else:
        axes[i].set_ylabel("")
        axes[i].set_yticklabels([])

plt.show()
../_images/notebooks_10x_xenium_focus_110_0.png

Mutually exclusive co-expression rate (MECR)

The mutually exclusive co-expression rate (MECR) is a measure for whether combinations of positive and negative markers (computed with a more stringent setting to increase mutual exclusivity, vote_frac_pos=0.3) co-occur less often than expected under independence (using Fisher’s exact test). By conditioning on the marginal detection frequencies of each gene, Fisher’s exact test does not favor methods with low overall transcript counts.

[59]:
markers = {}

for method, st in st_dict.items():
    markers[method] = st.markers_from_reference(
        adata_ref,
        ref_cell_type="celltype_major",
        min_pos_frac=0.3,
        ref_raw_counts_layer="raw",
        n_jobs=16,
    )

    mecr = st.sp.mutually_exclusive_coexpression_rate(
        markers=markers[method],
    )
WARNING: adata.X seems to be already log-transformed.
WARNING: adata.X seems to be already log-transformed.
WARNING: adata.X seems to be already log-transformed.

Across all methods, marker pairs with significant mutual exclusivity show substantially lower co-expression than expected under independence. Although the differences are minor, ProSeg shows the strongest depletion of co-expression among these marker pairs.

[60]:
rows = []

for method, st in st_dict.items():
    tbl = st.sdata.tables["table"]
    mecr_df = tbl.uns["mutually_exclusive_coexpression_rate"]

    # Keep marker pairs with significant mutual exclusivity
    df_sig = mecr_df.loc[
        mecr_df["odds_ratio"].notna()
        & np.isfinite(mecr_df["odds_ratio"])
        & mecr_df["pvalue"].notna()
        & np.isfinite(mecr_df["pvalue"])
        & (mecr_df["odds_ratio"] < 1)
        & (mecr_df["pvalue"] < 0.05)
    ].copy()

    rows.extend(
        {
            "method": str(method),
            "Fisher_OR": row["odds_ratio"],
        }
        for _, row in df_sig.iterrows()
    )

df = pd.DataFrame(rows)

# Order methods by mean OR
mean_order = df.groupby("method")["Fisher_OR"].mean().sort_values().index.tolist()

means = df.groupby("method")["Fisher_OR"].mean().reindex(mean_order)

xtick_labels = [f"{m}\nmean: {means[m]:.2f}" for m in mean_order]

plt.figure(figsize=(6, 4))

ax = sns.violinplot(
    data=df,
    x="method",
    y="Fisher_OR",
    order=mean_order,
    palette="Set2",
    linewidth=2,
)

sns.stripplot(
    data=df,
    x="method",
    y="Fisher_OR",
    order=mean_order,
    color="black",
    size=2.5,
    alpha=0.35,
    jitter=0.25,
    ax=ax,
)

# OR = 1 corresponds to independence
ax.axhline(
    1,
    ls="--",
    lw=1,
    color="gray",
    alpha=0.6,
)

ax.set_xticklabels(xtick_labels)

ax.set_ylabel("Mutually exclusive marker co-expression (odds ratio)")
ax.set_xlabel("")
ax.set_title("Significantly mutually exclusive marker pairs")

plt.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
../_images/notebooks_10x_xenium_focus_115_0.png

3D Volume Module#

The volume (vl) accessor provides metrics to assess how well a segmentation method resolves cell overlaps in 3D. Spatial transcriptomics tissue sections have a finite thickness (~4–10 µm), so cells can overlap along the z-dimension and 2D segmentation methods may introduce mixing by assigning transcripts from overlapping cells to the same mask. In this module, we introduce metrics to quantify sensitivity to 3D overlap and evaluate how well quasi-3D methods (e.g. Proseg) disentangle transcripts from overlapping cells.

For a detailed description of this module, please refer to this tutorial.

Top-bottom z consistency

To detect potential z-overlap mixing within segmented cells, we compare gene expression profiles between the bottom and top z-regions of each cell. similarity_top_bottom reports the observed cosine similarity relative to its permutation-based null expectation, with negative residuals indicating lower consistency across z than expected. In the histogram below, we show these residuals only for cells with unexpectedly low top–bottom similarity (similarity_top_bottom_p_value < 0.05).

[61]:
for _method, st in st_dict.items():
    _cos_sim = st.vl.similarity_top_bottom()

Proseg (a quasi-3D method) shows the fewest cells with unexpectedly different expression profiles between the top and bottom of the cell (similarity_top_bottom_p_value < 0.05). Moreover, even among these cells, the differences between top and bottom are generally smaller than for Xenium and BIDCell, suggesting more consistent transcript composition across z.

[62]:
histogram_feature(st_dict, "similarity_top_bottom")
../_images/notebooks_10x_xenium_focus_120_0.png

In volume.ipynb, we explore the distribution of transcripts along the z-dimension in more depth and put it into context with ovrlpy, a package for detecting 3D overlap in spatial transcriptomics.

Point statistic metrics#

The point statistics (ps) module is designed to compare the distribution of a set of transcripts in the cell relative to its cell centroid or cell border. The idea is to compute the distances of the transcripts to a reference point in the cell, either the cell centroid or the cell boundaries and aggregate this measure per transcript id.

distance to membrane metric

In this metric we compute the distance to the segmented cell membrane of each transcript coordinate and aggregate this metric per transcript id as mean.

[63]:
plt.style.use("dark_background")


def plot_metric(st, method, metric, genes, ax):
    sdata_plot = sd.deepcopy(st.sdata)
    sdata_plot[st.tables_key] = sdata_plot[st.tables_key][(sdata_plot[st.tables_key].obs[metric].notna())]

    sdata_plot.tables[st.tables_key].obs["region"] = st.shapes_key
    sdata_plot.set_table_annotates_spatialelement(st.tables_key, region=st.shapes_key)

    sdata_plot.pl.render_shapes(
        element=st.nucleus_shapes_key,
        fill_alpha=0.2,
        outline_alpha=1.0,
        outline_width=0.5,
        outline_color="black",
    ).pl.render_shapes(
        element=st.shapes_key,
        color=metric,
        cmap="viridis",
        fill_alpha=0.5,
        outline_alpha=1.0,
        outline_width=0.5,
        outline_color="black",
    ).pl.render_points(
        st.points_key,
        color=st.points_gene_key,
        groups=genes,
        palette=["red"] * len(genes),
    ).pl.show(ax=ax, title="Cell/nuclei seg. for " + method + " " + metric, colorbar=True)

    return fig, axes

First, we compute both the average distance to the cell membrane across all previously defined negative and positive markers for the cell type “DCIS1”.

[64]:
border_distance_negative = {}
border_distance_positive = {}

for method, st in st_dict.items():
    border_distance_negative[method] = st.ps.distance_to_membrane(
        markers[method]["DCIS1"]["negative"],
        cell_type_key="transferred_celltype_plot",
        cell_type_query=["DCIS1"],
        restrict_to_within_boundary=True,
        inplace=False,
    )
    border_distance_positive[method] = st.ps.distance_to_membrane(
        markers[method]["DCIS1"]["positive"],
        cell_type_key="transferred_celltype_plot",
        restrict_to_within_boundary=True,
        cell_type_query=["DCIS1"],
        inplace=False,
    )
[65]:
keys = []
values = []

for key, df in border_distance_negative.items():
    for val in df["distance_to_cell_membrane_norm_166_genes"]:
        keys.append(key)
        values.append(val)

result_negative = pd.DataFrame({"method": keys, "distance": values, "marker": "negative"})

keys = []
values = []

for key, df in border_distance_positive.items():
    for val in df["distance_to_cell_membrane_norm_78_genes"]:
        keys.append(key)
        values.append(val)

result_positive = pd.DataFrame({"method": keys, "distance": values, "marker": "positive"})
[66]:
result = pd.concat([result_negative, result_positive])
result = result.dropna()
[67]:
plt.figure(figsize=(6, 4))

ax = sns.violinplot(
    data=result,
    x="method",
    y="distance",
    hue="marker",
    palette="Set2",
    linewidth=2,
)

ax.set_ylabel("distance to membrane")
ax.set_xlabel("")
ax.set_title("Distance to membrane across cells for DCIS1")

plt.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
../_images/notebooks_10x_xenium_focus_128_0.png

We can plot now the distance to membrane for the positive and negative markers across the different segmentation algorithms and for the cell type “DCIS1”.

We can also run the method inplace and plot the results in space

[68]:
for _method, st in st_dict.items():
    st.ps.distance_to_membrane(
        markers[method]["DCIS1"]["negative"],
        cell_type_key="transferred_celltype_plot",
        cell_type_query=["DCIS1"],
        restrict_to_within_boundary=True,
        inplace=True,
    )
[69]:
fig, axes = plt.subplots(1, 3, figsize=(15, 10), constrained_layout=True)

for i, (method, st) in enumerate(st_dict.items()):
    plot_metric(
        st,
        method,
        "distance_to_cell_membrane_norm_166_genes",
        markers[method]["DCIS1"]["negative"][:20],
        axes[i],
    )
../_images/notebooks_10x_xenium_focus_132_0.png

We can identify cells in which the distribution of negative markers is clearly more skewed than for thers and that these values differ again greatly between the segmentation algorithms.

Metrics orthogonolisation#

In order to look at the co-linearity of the computed metrics, we will compute the correlation matrix (scaled covariance matrix) of all the metrics. Since the directionality (positively or negatively correlated) does not matter, we take the absolute value of the correlation coefficient as our orthogonality metric. Like this, we can visualise which metrics correlate

[70]:
# code adapted from claude.ai
for method, st in st_dict.items():
    df = st.sdata.tables["table"].obs
    df_numeric = df.select_dtypes(include="number")
    df = pd.DataFrame(df_numeric, columns=df_numeric.columns, index=df_numeric.index)
    # correlation does not work with NA values. Fill NA with columnwise mean
    df = df.apply(lambda x: x.fillna(x.mean()), axis=0)
    correlation_matrix = df.corr()

    # take absolute value instead - sign is not important, only co-linearity
    correlation_matrix = correlation_matrix.abs()

    plt.figure(figsize=(10, 8))
    sns.heatmap(
        correlation_matrix,
        annot=False,
        cmap="coolwarm",
        center=0,
        square=True,
        linewidths=0.5,
        cbar_kws={"shrink": 0.8},
        vmin=0,
        vmax=1,
        xticklabels=correlation_matrix.columns,
        yticklabels=correlation_matrix.columns,
    )
    plt.xticks(fontsize=7)
    plt.yticks(fontsize=7)
    plt.title(f"Absolute Correlation Matrix Heatmap {method}")
    plt.tight_layout()
    plt.show()
../_images/notebooks_10x_xenium_focus_136_0.png
../_images/notebooks_10x_xenium_focus_136_1.png
../_images/notebooks_10x_xenium_focus_136_2.png

There are quite some metrics which correlate with the segmented cell area, such as total counts of all transcripts and metrics related to this. We note as well that the correlation of metrics differs between algorithms.

Session Info#

[71]:
print(sd.__version__)  # spatialdata
print(spatialdata_plot.__version__)
0.8.0
0.4.0