Cohort primer¶

In [1]:
slug = "human-colon"
release = "2026-10-28"
use_gpu = True
out_dir = "bundles"
smoke = 0
primers_ref = "feat-primers"
cohorts_path = None
catalog_path = None
representatives = 1
dev_note = ""
In [2]:
# Parameters
slug = "human-ovarian-cancer"
release = "2026-11-01"
out_dir = "bundles"
primers_ref = "4d7af340849c00077f9d21c5b54af9e689a4a070"
dev_note = ""

Human ovarian cancer¶

Cohort human-ovarian-cancer, catalog release 2026-11-01.

This primer describes a cohort; it is not a benchmark. It does not rank papers, technologies or methods. The batch correction (Harmony on the section) is shown for orientation only: well-mixed embeddings are not proof of a correct biology. This cohort's technologies may also differ by paper and lab. Where one paper used one technology, a technology effect can't be separated from a paper effect.

The analysis below runs the primers package at a fixed reference. To install the same code:

pip install "sddb-primers @ git+https://github.com/theislab/spatialdata-db-client@4d7af340849c00077f9d21c5b54af9e689a4a070#subdirectory=primers"

Setup¶

The imports, and a helper that links each primers function to its source at the reference that ran.

In [5]:
import inspect
import json
import platform
import time

import anndata as ad
import numpy as np
import pandas as pd
import scanpy as sc
import spatialdata_plot  # noqa: F401  (registers the .pl accessor)
import zarr
from primers.cohort import hd_table, load_cohort, smoke_spec
from primers.embed import CAVEAT, MAX_FLOOR_LOSS, MIN_SHARED_GENES, N_HVG, N_NEIGHBORS, N_PCS, SEED, embed_views
from primers.export import export_bundle, mixing_by_view, with_qc_status
from primers.markers import load_markers, panel_id
from primers.qc import MIN_GENES_FOR_QC, apply_qc, load_thresholds, qc_floors
from spatialdata.transformations import get_transformation

import sddb
from sddb.remote import storage_options

start = time.time()
out_dir = Path(out_dir)
if use_gpu:
    import cupy as cp
    import rapids_singlecell as rsc
    import rmm
    from rmm.allocators.cupy import rmm_cupy_allocator

    rmm.reinitialize(managed_memory=True)
    cp.cuda.set_allocator(rmm_cupy_allocator)


def show_call(func, what):
    """Show what a primers function does and link its source at ``primers_ref``."""
    _, line = inspect.getsourcelines(func)
    module = func.__module__.split(".")[-1]
    url = f"{REPO}/blob/{primers_ref}/primers/primers/{module}.py#L{line}"
    display(Markdown(f"`{func.__module__}.{func.__name__}`: {what} [Source at `{primers_ref}`]({url})."))


def show_table(df, digits=3):
    """Display ``df`` rounded, with missing values written as n/a, and long text columns in full."""
    shown = df.round(digits).astype(object)
    with pd.option_context("display.max_colwidth", None):
        display(shown.where(df.notna(), "n/a"))


catalog = sddb.Catalog.from_file(catalog_path) if catalog_path else sddb.Catalog(version=release)

Query and cohort¶

The cohort is a stated catalog query, plus the uids it includes and minus the uids it excludes. The spec below is the one in cohorts.yaml.

In [6]:
if smoke:
    show_call(
        smoke_spec,
        "Keeps only the smallest kept member of each of a few papers, taken in turn from each family, for a "
        "quick end-to-end check.",
    )
    n_spec_excluded = len(spec.get("exclude_uids") or [])
    spec = smoke_spec(spec, catalog, smoke)
    print(
        f"Smoke run on {smoke} papers: {len(spec['exclude_uids']) - n_spec_excluded} more uids are excluded "
        f"({len(spec['exclude_uids'])} in all)."
    )
for key in ("slug", "query", "include_uids", "exclude_uids", "families", "marker_set", "rationale"):
    value = spec.get(key)
    if smoke and key == "exclude_uids":
        value = f"{len(value)} uids"
    print(f"{key}: {value}")
slug: human-ovarian-cancer
query: {'organism': 'human', 'tissue': 'ovary', 'n_obs__lte': 500000}
include_uids: None
exclude_uids: ['cmp4e4nFkMWds59W0000', '8tzX3kVY3InHDexb0000']
families: ['spot', 'imaging']
marker_set: ovary
rationale: Mostly high-grade serous ovarian cancer on Visium, Visium HD and Xenium; the cap leaves out one 1.8M-bin HD and one 1.2M-cell Xenium section.
In [7]:
show_call(
    load_cohort,
    "Runs the query on the catalog, adds the included uids, removes the excluded ones, and loads each kept "
    "member's table keyed by gene symbol, joined within its technology family.",
)
adatas, cohort = load_cohort(spec, catalog=catalog)

primers.cohort.load_cohort: Runs the query on the catalog, adds the included uids, removes the excluded ones, and loads each kept member's table keyed by gene symbol, joined within its technology family. Source at 4d7af340849c00077f9d21c5b54af9e689a4a070.

In [8]:
columns = ["uid", "title", "technology", "family", "paper", "paper_source", "license", "n_obs", "status"]
show_table(cohort[columns].rename(columns={"license": "licence"}))
show_table(cohort["status"].value_counts().rename_axis("status").reset_index(name="members"))
print(f"{int((cohort['status'] == 'kept').sum())} kept, {int((cohort['status'] != 'kept').sum())} dropped.")
uid title technology family paper paper_source licence n_obs status
0 haOzFbJ7Bc9x7NW60001 visium-hd-three-prime-ovarian-cancer-discovery-fresh-frozen VisiumHD spot n/a none CC-BY-4.0 475742 dropped: no paper (no study_id, series or collection)
1 lkmeOv5OovP3QHaL0000 visium-hd-cytassist-gene-expression-ovarian-cancer-discovery-fresh-frozen VisiumHD spot 10x Genomics demo datasets 10x_demo CC-BY-4.0 445751 kept
2 2J5hRAf9Rr69DAZj0000 human-ovarian-cancer-11-mm-capture-area-ffpe-2-standard Visium spot 10x Genomics demo datasets 10x_demo CC-BY-4.0 4674 kept
3 V4PqcXdb83DmtG2k0000 human-ovarian-cancer-1-standard Visium spot 10x Genomics demo datasets 10x_demo CC-BY-4.0 3455 kept
4 5vX34BZOVH4KXbSL0000 human-ovarian-cancer-whole-transcriptome-analysis-stains-dapi-anti-pan-ck-anti-cd-45-1-standard-1-2-0 Visium spot 10x Genomics demo datasets 10x_demo CC-BY-4.0 3493 kept
... ... ... ... ... ... ... ... ... ...
98 6adER8iy0hUhrFlb0000 GSM8207501 Visium spot geo-gse263920 geo_series ncbi-geo-policy 2644 kept
99 Ex8cQhFw9SbcVV7M0000 GSM9535607 Xenium imaging_cell 10.1038/s41467-026-72036-0 doi ncbi-geo-policy 110570 kept
100 FVBEfvqtphqJQlVD0000 GSM9535606 Xenium imaging_cell 10.1038/s41467-026-72036-0 doi ncbi-geo-policy 355346 kept
101 aWgoDlPo2wqLqUob0000 GSM9981856 Xenium imaging_cell geo-gsm9981856 study_id ncbi-geo-policy 437136 kept
102 Y0zKMV4B6GT4Hxv70000 GSM9981860 Xenium imaging_cell geo-gsm9981860 study_id ncbi-geo-policy 388133 kept

103 rows × 9 columns

status members
0 kept 100
1 dropped: no paper (no study_id, series or collection) 3
100 kept, 3 dropped.

The batch variable is the paper. A member's paper is its DOI. Without a DOI it is its GEO series, else its first collection, else its study id. A DOI that one member of a series or collection carries is given to the other members of that group. All 10x Genomics demo datasets count as one paper, because they come from one company and not from independent labs. Harmony runs on the section (dataset_uid), not on the paper. 10x often publishes one section in several Space Ranger releases, and only the newest release is kept, and the others are dropped as re-releases. Serial sections of one 10x block count as one section in the mixing statistics.

Quality control¶

Each technology group has its own filters. The table gives the values and the reason for each group.

In [9]:
show_call(load_thresholds, "Reads the QC threshold groups from thresholds.yaml.")
thresholds = load_thresholds()
show_table(
    pd.DataFrame(
        [
            {
                "group": name,
                "technologies": ", ".join(g["technologies"]),
                "min_counts": g["min_counts"],
                "min_genes": g["min_genes"],
                "max_pct_mt": g.get("max_pct_mt"),
                "flag: min median counts": g["flag"]["min_median_counts"],
                "flag: min share kept": g["flag"]["min_share_kept"],
                "reason": g["reason"],
            }
            for name, g in thresholds.items()
        ]
    ).set_index("group")
)
print(f"MIN_GENES_FOR_QC = {MIN_GENES_FOR_QC}: QC, and every view, needs at least this many genes.")

primers.qc.load_thresholds: Reads the QC threshold groups from thresholds.yaml. Source at 4d7af340849c00077f9d21c5b54af9e689a4a070.

technologies min_counts min_genes max_pct_mt flag: min median counts flag: min share kept reason
group
spot Visium, SpatialTranscriptomics 500 250 30.0 1000 0.5 A Visium or ST spot holds about 1 to 10 cells, so a spot with under 500 UMIs or 250 genes is mostly background or tissue edge; solid tissues often reach 20 to 30% mitochondrial reads, so the cap is 30%.
binned_spot VisiumHD 200 50 30.0 200 0.5 On the 5 Visium HD sections of oliveira-2024-colorectal, 16 um bins at 200 counts kept 74% of bins with the same marker signal as 8 um bins at Oliveira et al.'s 100 (Nat Genet 57:1512-1523), which kept 60%, and found T cells in all 5 sections against 3 (web tasks/2026-11-01-t094-hd-bin-size.md).
imaging_cell Xenium, Merscope, MERFISH 10 3 n/a 30 0.5 Targeted panels give tens to hundreds of decoded transcripts per segmented cell, and a cell with under 10 transcripts or 3 genes cannot be given a type; the panels have no mitochondrial genes.
MIN_GENES_FOR_QC = 50: QC, and every view, needs at least this many genes.
In [10]:
show_call(
    apply_qc,
    "Leaves out control probes, computes counts, genes and the mitochondrial share per observation, keeps "
    "the observations that pass their technology's filters, and describes each dataset.",
)
qcs = []
for fam in list(adatas):
    adatas[fam], q = apply_qc(adatas[fam], thresholds)
    qcs.append(q.assign(family=fam))
qc = pd.concat(qcs, ignore_index=True)
show_table(qc)

primers.qc.apply_qc: Leaves out control probes, computes counts, genes and the mitochondrial share per observation, keeps the observations that pass their technology's filters, and describes each dataset. Source at 4d7af340849c00077f9d21c5b54af9e689a4a070.

dataset_uid technology n_before n_after median_counts median_genes pct_mt_median flagged flag_reason status family
0 0V6JJeQ8XBhHawNd0000 Visium 1739 1732 9799.0 5614.0 3.29 False kept spot
1 0dKt68GDRv8sSFFe0000 Visium 987 985 18148.0 4818.0 3.727 False kept spot
2 1KOUlpST5EqHJdGZ0000 Visium 1504 1501 7194.0 3062.0 4.763 False kept spot
3 1fqaI0w4rdOJp4SR0000 Visium 1977 1977 7986.0 3542.0 0.0 False kept spot
4 1ryCAbWEtsGEW5Zw0000 Visium 3632 3594 4251.5 1915.0 6.139 False kept spot
... ... ... ... ... ... ... ... ... ... ... ...
95 Ex8cQhFw9SbcVV7M0000 Xenium 110570 104859 39.0 30.0 n/a False kept imaging_cell
96 FVBEfvqtphqJQlVD0000 Xenium 355346 351976 76.0 44.0 n/a False kept imaging_cell
97 Y0zKMV4B6GT4Hxv70000 Xenium 388133 350394 90.0 48.0 n/a False kept imaging_cell
98 aWgoDlPo2wqLqUob0000 Xenium 437136 435144 218.0 84.0 n/a False kept imaging_cell
99 d4yjKUQ98oi2aGPT0001 Xenium 205082 204224 595.0 116.0 n/a False kept imaging_cell

100 rows × 11 columns

In [11]:
MAX_VIOLIN_DATASETS = 40
for fam, adata in adatas.items():
    obs = adata.obs[["dataset_uid", "total_counts", "n_genes"]].copy()
    sizes = obs["dataset_uid"].value_counts()
    if len(sizes) > MAX_VIOLIN_DATASETS:
        print(f"{fam}: {len(sizes)} datasets, so the {MAX_VIOLIN_DATASETS} largest are shown.")
        obs = obs[obs["dataset_uid"].isin(sizes.index[:MAX_VIOLIN_DATASETS])]
    obs["dataset_uid"] = obs["dataset_uid"].astype(str).astype("category")
    display(Markdown(f"Counts and genes per observation after QC, family `{fam}`."))
    sc.pl.violin(ad.AnnData(obs=obs), ["total_counts", "n_genes"], groupby="dataset_uid", rotation=90, stripplot=False)
spot: 94 datasets, so the 40 largest are shown.

Counts and genes per observation after QC, family spot.

No description has been provided for this image

Counts and genes per observation after QC, family imaging_cell.

No description has been provided for this image

Marker set¶

Compartments are named only from this marker set, which was fixed before any embedding was computed. A compartment that a panel misses can have a fallback chosen from that panel's genes.

In [12]:
show_call(load_markers, "Reads the marker sets from markers.yaml, each with its citation and fallbacks.")
marker_set = load_markers()[spec["marker_set"]]
print(f"Marker set {spec['marker_set']!r}, cited from {', '.join(marker_set['citation'])}.")
show_table(
    pd.DataFrame({"markers": {c: ", ".join(g) for c, g in marker_set["compartments"].items()}}).rename_axis(
        "compartment"
    )
)
kept_after_qc = set(qc.loc[qc["status"] == "kept", "dataset_uid"].astype(str))
technology_of = cohort.set_index("uid")["technology"].astype(str)
panel_of = {
    uid: panel_id(technology_of[uid], genes)
    for adata in adatas.values()
    for uid, genes in adata.uns.get("member_genes", {}).items()
    if uid in kept_after_qc
}
used = [
    {
        "panel": pid,
        "compartment": comp,
        "genes": "not measured" if fb.get("not_measured") else ", ".join(fb.get("genes") or []),
        "note": fb.get("note", ""),
    }
    for pid, entries in (marker_set.get("fallback") or {}).items()
    if pid in set(panel_of.values())
    for comp, fb in entries.items()
]
if used:
    show_table(pd.DataFrame(used))
else:
    print("No fallback of this marker set applies to the panels of this cohort.")

primers.markers.load_markers: Reads the marker sets from markers.yaml, each with its citation and fallbacks. Source at 4d7af340849c00077f9d21c5b54af9e689a4a070.

Marker set 'ovary', cited from 10.1038/s41586-022-05496-1.
markers
compartment
cancer_epithelial PAX8, EPCAM, MUC16
fibroblast COL1A1, DCN, LUM
endothelial PECAM1, VWF, CDH5
t_cell CD3E, CD8A, CD2
b_plasma MS4A1, CD79A, MZB1
myeloid CD68, CD163, LYZ
panel compartment genes note
0 Xenium-5001-079a3380 fibroblast PDGFRA, FAP, POSTN fibroblast and CAF genes on this panel

Spatial overview¶

For each technology, the kept section with the median number of observations is drawn, coloured by one marker gene of the first compartment whose markers (or its panel's fallbacks) the table measures. Only the table, the shapes it annotates and, for spot technologies, the low-resolution image are loaded.

In [13]:
show_call(with_qc_status, "Marks the members that have no cells left after QC as dropped.")
kept_members = with_qc_status(cohort, qc).query("status == 'kept'")
ENSEMBL = r"ENS[A-Z]*G\d{6}"
SYMBOL_COLUMNS = ("symbol", "gene_symbols", "gene_symbol", "gene_name", "feature_name")


def colour_gene(table, panel):
    """(var name, symbol) of the first gene of the first compartment the table measures.

    A compartment's genes are its markers, then the fallback genes for this section's own panel.
    """
    names = pd.Index(table.var_names.astype(str))
    symbols = names
    if names.str.match(ENSEMBL).mean() >= 0.5:
        column = next((c for c in SYMBOL_COLUMNS if c in table.var), None)
        if column is None:
            return None, None
        symbols = pd.Index(table.var[column].astype(str))
    by_symbol = {s.upper(): (n, s) for n, s in zip(names, symbols, strict=True)}
    fallback = (marker_set.get("fallback") or {}).get(panel) or {}
    for comp, genes in marker_set["compartments"].items():
        for gene in [*genes, *((fallback.get(comp) or {}).get("genes") or [])]:
            if gene.upper() in by_symbol:
                return by_symbol[gene.upper()]
    return None, None


def table_region(zarr_url, table_name):
    """The shapes element a table annotates, read from the table's zarr attributes without loading it."""
    group = zarr.open_group(zarr_url, mode="r", storage_options=storage_options(zarr_url) or None)
    attrs = dict(group[f"tables/{table_name}"].attrs)
    if "region" not in attrs and f"tables/{table_name}/uns/spatialdata_attrs" in group:
        attrs = ad.io.read_elem(group[f"tables/{table_name}/uns/spatialdata_attrs"])
    region = attrs.get("region")
    return region if isinstance(region, str) or region is None else (list(region) or [None])[0]


def representative_uids(members):
    """The ``representatives`` kept sections closest to the median number of observations."""
    n_obs = members.set_index("uid")["n_obs"].astype(float)
    return list((n_obs - n_obs.median()).abs().sort_values(kind="stable").index[:representatives])

primers.export.with_qc_status: Marks the members that have no cells left after QC as dropped. Source at 4d7af340849c00077f9d21c5b54af9e689a4a070.

In [14]:
for tech, members in kept_members.groupby("technology"):
    for uid in representative_uids(members):
        ds = catalog.dataset(uid)
        stored = ds.elements()
        table_name = hd_table(stored) if tech == "VisiumHD" else (getattr(ds, "default_table", None) or "table")
        region = table_region(ds.zarr_url, table_name)
        if region is None or f"shapes/{region}" not in stored:
            display(Markdown(f"{tech}, section `{uid}`: the table annotates no shapes element, so no plot."))
            continue
        # a small tissue image when there is one: lowres, else hires (never the full-resolution image)
        images = [k.split("/", 1)[1] for k in stored if k.startswith("images/")]
        lowres = next((k for k in images if "lowres" in k), None) or next((k for k in images if "hires" in k), None)
        wanted = [f"tables/{table_name}", f"shapes/{region}"] + ([f"images/{lowres}"] if lowres else [])
        sdata = ds.to_sdata(elements=wanted)
        gene, symbol = colour_gene(sdata[table_name], panel_of.get(uid))
        section = members.set_index("uid").loc[uid, "title"]
        title = f"{tech}, {section}: {symbol}" if gene else f"{tech}, {section}"
        display(Markdown(f"{tech}, section `{uid}` ({int(members.set_index('uid').loc[uid, 'n_obs'])} observations)."))
        systems = list(get_transformation(sdata.shapes[region], get_all=True))
        system = "global" if "global" in systems else systems[0]
        # spatialdata-plot 0.4.2 rasterises shapes with datashader in "global" only; elsewhere use matplotlib
        method = None if system == "global" else "matplotlib"
        # fmt: off
        if lowres:
            (
                sdata
                .pl.render_images(lowres)
                .pl.render_shapes(region, color=gene, table_name=table_name, method=method, colorbar_params={"label": symbol})
                .pl.show(
                    coordinate_systems=system,
                    title=title,
                )
            )
        else:
            (
                sdata
                .pl.render_shapes(region, color=gene, table_name=table_name, method=method, colorbar_params={"label": symbol})
                .pl.show(
                    coordinate_systems=system,
                    title=title,
                )
            )
        # fmt: on
2026-11-04 03:53:45 | [INFO] root_attr: version
2026-11-04 03:53:45 | [INFO] root_attr: multiscales
2026-11-04 03:53:45 | [INFO] root_attr: omero
2026-11-04 03:53:45 | [INFO] datasets [{'path': 's0', 'coordinateTransformations': [{'type': 'scale', 'scale': [1.0, 1.0, 1.0]}, {'type': 'translation', 'translation': [0.0, 0.0, 0.0]}]}]
2026-11-04 03:53:45 | [INFO] resolution: s0
2026-11-04 03:53:45 | [INFO]  - shape ('c', 'y', 'x') = (3, 600, 555)
2026-11-04 03:53:45 | [INFO]  - chunks =  ['3', '600', '555']
2026-11-04 03:53:45 | [INFO]  - dtype = uint8
2026-11-04 03:53:45 | [INFO] root_attr: version
2026-11-04 03:53:45 | [INFO] root_attr: multiscales
2026-11-04 03:53:45 | [INFO] root_attr: omero

Visium, section Ov3FhASVNetyR3aE0000 (1661 observations).

2026-11-04 03:54:03 | [INFO] root_attr: version
2026-11-04 03:54:03 | [INFO] root_attr: multiscales
2026-11-04 03:54:03 | [INFO] root_attr: omero
2026-11-04 03:54:03 | [INFO] datasets [{'path': 's0', 'coordinateTransformations': [{'type': 'scale', 'scale': [1.0, 1.0, 1.0]}, {'type': 'translation', 'translation': [0.0, 0.0, 0.0]}]}]
2026-11-04 03:54:03 | [INFO] resolution: s0
2026-11-04 03:54:03 | [INFO]  - shape ('c', 'y', 'x') = (3, 600, 490)
2026-11-04 03:54:03 | [INFO]  - chunks =  ['3', '600', '490']
2026-11-04 03:54:03 | [INFO]  - dtype = uint8
2026-11-04 03:54:03 | [INFO] root_attr: version
2026-11-04 03:54:03 | [INFO] root_attr: multiscales
2026-11-04 03:54:03 | [INFO] root_attr: omero

VisiumHD, section lkmeOv5OovP3QHaL0000 (445751 observations).

Xenium, section d4yjKUQ98oi2aGPT0001 (205082 observations).

No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Embedding¶

Each technology family is embedded on the genes every member measured. A joint view across families is added only when the gates pass. The parameters of the embedding come first.

In [15]:
show_call(
    qc_floors, "Gives each technology's count floor, the min_counts of its QC group, for gate 3 and the joint view."
)
floors = qc_floors(thresholds)
show_table(
    pd.DataFrame(
        {
            "value": [str(x) for x in (MIN_SHARED_GENES, MAX_FLOOR_LOSS, N_HVG, N_PCS, N_NEIGHBORS, SEED)],
            "meaning": [
                "genes a view needs before a family is split per technology, and the joint view's gate 1",
                "largest share of a technology's observations that may fall below its floor on the shared genes (gate 3)",
                "highly variable genes kept when a view has more genes",
                "principal components",
                "neighbours per observation",
                "random seed",
            ],
        },
        index=["MIN_SHARED_GENES", "MAX_FLOOR_LOSS", "N_HVG", "N_PCS", "N_NEIGHBORS", "SEED"],
    )
)
show_table(pd.Series(floors, name="count floor").rename_axis("technology").to_frame())

primers.qc.qc_floors: Gives each technology's count floor, the min_counts of its QC group, for gate 3 and the joint view. Source at 4d7af340849c00077f9d21c5b54af9e689a4a070.

value meaning
MIN_SHARED_GENES 200 genes a view needs before a family is split per technology, and the joint view's gate 1
MAX_FLOOR_LOSS 0.5 largest share of a technology's observations that may fall below its floor on the shared genes (gate 3)
N_HVG 2000 highly variable genes kept when a view has more genes
N_PCS 30 principal components
N_NEIGHBORS 30 neighbours per observation
SEED 0 random seed
count floor
technology
Visium 500.0
SpatialTranscriptomics 500.0
VisiumHD 200.0
Xenium 10.0
Merscope 10.0
MERFISH 10.0
In [16]:
show_call(
    embed_views,
    "Builds one view per family (or per technology when a family shares too few genes), plus the joint view "
    "when the gates pass, and embeds each with PCA, UMAP and Harmony on the section.",
)
views = embed_views(adatas, marker_set, floor=floors, use_gpu=use_gpu)
for name, v in views.items():
    p = v.uns["primer"]
    print(f"{name}: {p['n_obs']} observations, {p['n_sections']} sections; {p['genes']['rule']}. {p['note']}".strip())
first = next(iter(views.values())).uns["primer"]
for uid, reason in first["members_dropped_panel"].items():
    print(f"member {uid} {reason}")
for c in {tuple(c["uids"]): c for v in views.values() for c in v.uns["primer"]["panel_costs"]}.values():
    print(f"{c['technology']} panel of {', '.join(c['uids'])}: {c['reason']} shared genes")
for name, reason in first["views_skipped"].items():
    print(f"no view {name}: {reason}")
gates = next((v.uns["primer"]["gates"] for v in views.values() if "gates" in v.uns["primer"]), None)
if gates is not None:
    print(f"Joint view: {'yes' if gates['joint'] else 'no'}. {gates['reason']}".strip())
    for drop in gates["drops"]:
        print(f"  left out {drop['technology']} ({drop['step']}): {drop['reason']}")

primers.embed.embed_views: Builds one view per family (or per technology when a family shares too few genes), plus the joint view when the gates pass, and embeds each with PCA, UMAP and Harmony on the section. Source at 4d7af340849c00077f9d21c5b54af9e689a4a070.

spot: 370753 observations, 94 sections; 2000 highly variable genes of the 16850 measured, expressed genes (seurat_v3 on raw counts, section as batch); its 6 gene panels have 17,943 to 38,606 genes each and share 16,851.
imaging_cell: 1647304 observations, 6 sections; all 96 measured, expressed genes of the view; its 4 gene panels have 380 to 5,001 genes each and share 96. only 96 shared genes (under 200)
Joint view: no. fewer than two families keep a technology
  left out VisiumHD (gate3): loses 99% of its observations below 200 counts on the 94 shared genes (limit 50%)
  left out Visium (gate3): loses 99% of its observations below 500 counts on the 94 shared genes (limit 50%)
In [17]:
for name, v in views.items():
    colours = [c for c in ("paper", "technology", "tissue") if c in v.obs and v.obs[c].nunique() > 1]
    display(Markdown(f"View `{name}`, before Harmony."))
    sc.pl.umap(v, color=colours or None, ncols=1)
    if "X_umap_harmony" in v.obsm:
        display(Markdown(f"View `{name}`, after Harmony on the section."))
        sc.pl.embedding(v, basis="X_umap_harmony", color=colours or None, ncols=1)
    else:
        display(Markdown(f"View `{name}` has one section, so it has no Harmony embedding."))

View spot, before Harmony.

No description has been provided for this image

View spot, after Harmony on the section.

No description has been provided for this image

View imaging_cell, before Harmony.

No description has been provided for this image

View imaging_cell, after Harmony on the section.

No description has been provided for this image
In [18]:
show_call(
    mixing_by_view,
    "Measures how evenly the batches mix among each observation's nearest neighbours, per view, before and "
    "after Harmony; the bundle's manifest records the same numbers.",
)
mixing = mixing_by_view(views)
rows, definitions = [], {}
for name, by_batch in mixing.items():
    for label, by_basis in by_batch.items():
        for basis, m in by_basis.items():
            for stat in ("entropy", "other_share"):
                definitions.setdefault(stat, m[stat]["definition"].replace(m["batch_key"], "batch"))
                rows.append(
                    {
                        "view": name,
                        "batch": label,
                        "embedding": "after Harmony" if basis == "X_pca_harmony" else "before Harmony",
                        "statistic": stat,
                        "mean": m[stat]["mean"],
                        "random reference": m[stat]["random"],
                        "limit from proportions": m[stat]["random_limit"],
                        "note": m["note"],
                    }
                )
show_table(pd.DataFrame(rows))
for stat, text in definitions.items():
    print(f"{stat}: {text}")
print(CAVEAT)

primers.export.mixing_by_view: Measures how evenly the batches mix among each observation's nearest neighbours, per view, before and after Harmony; the bundle's manifest records the same numbers. Source at 4d7af340849c00077f9d21c5b54af9e689a4a070.

view batch embedding statistic mean random reference limit from proportions note
0 spot section before Harmony entropy 0.031 0.649 0.908
1 spot section before Harmony other_share 0.059 0.824 0.853
2 spot section after Harmony entropy 0.289 0.648 0.908
3 spot section after Harmony other_share 0.389 0.825 0.853
4 spot paper before Harmony entropy 0.008 0.561 0.64
5 spot paper before Harmony other_share 0.01 0.635 0.657
6 spot paper after Harmony entropy 0.243 0.56 0.64
7 spot paper after Harmony other_share 0.301 0.635 0.657
8 imaging_cell section before Harmony entropy 0.34 0.9 0.949
9 imaging_cell section before Harmony other_share 0.357 0.78 0.805
10 imaging_cell section after Harmony entropy 0.437 0.9 0.949
11 imaging_cell section after Harmony other_share 0.427 0.779 0.805
12 imaging_cell paper before Harmony entropy 0.333 0.959 0.997
13 imaging_cell paper before Harmony other_share 0.288 0.724 0.748
14 imaging_cell paper after Harmony entropy 0.428 0.96 0.997
15 imaging_cell paper after Harmony other_share 0.344 0.724 0.748
entropy: How evenly the batch labels are spread among each observation's 30 nearest neighbours, divided by log(min(30, number of labels)): 0 when all neighbours share one label, 1 when the labels are as evenly spread as the neighbours allow.
other_share: The share of each observation's 30 nearest neighbours that carry another batch.
Well mixed is not the same as biologically right: a correction can also mix cells that differ for a real reason, such as tissue or disease.

Biology¶

Each view is clustered with Leiden (resolution 0.5, 2 iterations) on its Harmony graph, or on its PCA graph when it is uncorrected. The clusters are then described by the marker genes of the marker set only.

In [19]:
fallback_genes = {}
for entries in (marker_set.get("fallback") or {}).values():
    for comp, fb in (entries or {}).items():
        fallback_genes.setdefault(comp, set()).update(str(g).upper() for g in (fb or {}).get("genes") or [])
compartment_genes = {
    comp: {str(g).upper() for g in genes} | fallback_genes.get(comp, set())
    for comp, genes in marker_set["compartments"].items()
}
readings = {}
for name, v in views.items():
    key = "nn_harmony" if "nn_harmony_distances" in v.obsp else "nn_pca"
    leiden = {"resolution": 0.5, "random_state": SEED, "neighbors_key": key, "key_added": "leiden"}
    if use_gpu:  # rapids-singlecell's n_iterations counts aggregation levels, so its default is kept
        rsc.tl.leiden(v, **leiden)
    else:
        sc.tl.leiden(v, flavor="igraph", n_iterations=2, **leiden)
    expr = v.obsm["marker_expr"]
    m = ad.AnnData(
        X=expr.to_numpy(dtype=np.float32),
        obs=v.obs[["leiden"]].copy(),
        var=pd.DataFrame(index=expr.columns.astype(str)),
    )
    display(Markdown(f"View `{name}`: {m.obs['leiden'].nunique()} clusters on `{key}`, {m.n_vars} marker genes."))
    if m.n_vars == 0 or m.obs["leiden"].nunique() < 2:
        display(Markdown("This view has fewer than two clusters or no marker gene, so there is no dotplot."))
        continue
    by_compartment = {
        comp: [g for g in expr.columns if g.upper() in genes] for comp, genes in compartment_genes.items()
    }
    sc.pl.dotplot(m, {k: g for k, g in by_compartment.items() if g}, groupby="leiden", standard_scale="var")
    scores = (
        pd.DataFrame(
            {
                comp: expr[[g for g in expr.columns if g.upper() in genes]].mean(axis=1)
                for comp, genes in compartment_genes.items()
                if any(g.upper() in genes for g in expr.columns)
            }
        )
        .groupby(m.obs["leiden"].to_numpy())
        .mean()
    )
    ranked = scores.apply(lambda r: r.sort_values(ascending=False), axis=1)
    table = pd.DataFrame(
        {
            "observations": m.obs["leiden"].value_counts().reindex(scores.index),
            "top compartment": scores.idxmax(axis=1),
            "top mean": scores.max(axis=1),
            "runner-up": [r.dropna().index[1] if r.notna().sum() > 1 else "" for _, r in ranked.iterrows()],
            "runner-up mean": [r.dropna().iloc[1] if r.notna().sum() > 1 else np.nan for _, r in ranked.iterrows()],
        }
    ).rename_axis("cluster")
    readings[name] = table
    show_table(table)

View spot: 14 clusters on nn_harmony, 21 marker genes.

No description has been provided for this image
observations top compartment top mean runner-up runner-up mean
cluster
0 44638 cancer_epithelial 0.73 cancer_epithelial 0.73
1 76839 cancer_epithelial 1.093 cancer_epithelial 1.093
10 1078 cancer_epithelial 0.914 cancer_epithelial 0.914
11 12386 cancer_epithelial 1.937 cancer_epithelial 1.937
12 61 cancer_epithelial 1.021 cancer_epithelial 1.021
13 1912 fibroblast 0.888 cancer_epithelial 0.273
2 40545 fibroblast 1.168 cancer_epithelial 0.488
3 10548 myeloid 0.918 cancer_epithelial 0.573
4 71740 fibroblast 1.62 cancer_epithelial 0.341
5 18400 fibroblast 1.214 cancer_epithelial 0.176
6 77447 cancer_epithelial 2.069 cancer_epithelial 2.069
7 3796 cancer_epithelial 1.743 cancer_epithelial 1.743
8 9525 fibroblast 1.016 cancer_epithelial 0.111
9 1838 cancer_epithelial 0.597 cancer_epithelial 0.597

View imaging_cell: 205 clusters on nn_harmony, 10 marker genes.

No description has been provided for this image
observations top compartment top mean runner-up runner-up mean
cluster
0 303734 cancer_epithelial 1.557 cancer_epithelial 1.557
1 185504 myeloid 0.476 cancer_epithelial 0.334
10 14923 cancer_epithelial 0.705 cancer_epithelial 0.705
100 237 cancer_epithelial 0.009 cancer_epithelial 0.009
101 94 cancer_epithelial 0.196 cancer_epithelial 0.196
... ... ... ... ... ...
95 91 cancer_epithelial 0.023 cancer_epithelial 0.023
96 157 b_plasma 1.016 cancer_epithelial 0.047
97 171 myeloid 1.245 cancer_epithelial 0.0
98 181 cancer_epithelial 0.0 cancer_epithelial 0.0
99 45 cancer_epithelial 0.0 cancer_epithelial 0.0

205 rows × 5 columns

Draft reading, to be reviewed by the Lead¶

Each statement below is generated from the reading table of its view and cites the numbers in it. The score of a compartment is the mean, over a cluster's observations, of the mean log-normalised expression of the compartment's marker and fallback genes. Compartments can sit inside others (goblet cells are epithelial), so the top compartment of a cluster does not exclude the runner-up.

In [20]:
for name, table in readings.items():
    lines = [f"View `{name}`:"]
    for cluster, r in table.iterrows():
        text = (
            f"- Cluster {cluster} ({int(r['observations'])} observations) has its highest mean marker expression "
            f"for {r['top compartment']} ({r['top mean']:.2f})"
        )
        if r["runner-up"]:
            text += f", followed by {r['runner-up']} ({r['runner-up mean']:.2f})"
        lines.append(text + ".")
    display(Markdown("\n".join(lines)))

View spot:

  • Cluster 0 (44638 observations) has its highest mean marker expression for cancer_epithelial (0.73), followed by cancer_epithelial (0.73).
  • Cluster 1 (76839 observations) has its highest mean marker expression for cancer_epithelial (1.09), followed by cancer_epithelial (1.09).
  • Cluster 10 (1078 observations) has its highest mean marker expression for cancer_epithelial (0.91), followed by cancer_epithelial (0.91).
  • Cluster 11 (12386 observations) has its highest mean marker expression for cancer_epithelial (1.94), followed by cancer_epithelial (1.94).
  • Cluster 12 (61 observations) has its highest mean marker expression for cancer_epithelial (1.02), followed by cancer_epithelial (1.02).
  • Cluster 13 (1912 observations) has its highest mean marker expression for fibroblast (0.89), followed by cancer_epithelial (0.27).
  • Cluster 2 (40545 observations) has its highest mean marker expression for fibroblast (1.17), followed by cancer_epithelial (0.49).
  • Cluster 3 (10548 observations) has its highest mean marker expression for myeloid (0.92), followed by cancer_epithelial (0.57).
  • Cluster 4 (71740 observations) has its highest mean marker expression for fibroblast (1.62), followed by cancer_epithelial (0.34).
  • Cluster 5 (18400 observations) has its highest mean marker expression for fibroblast (1.21), followed by cancer_epithelial (0.18).
  • Cluster 6 (77447 observations) has its highest mean marker expression for cancer_epithelial (2.07), followed by cancer_epithelial (2.07).
  • Cluster 7 (3796 observations) has its highest mean marker expression for cancer_epithelial (1.74), followed by cancer_epithelial (1.74).
  • Cluster 8 (9525 observations) has its highest mean marker expression for fibroblast (1.02), followed by cancer_epithelial (0.11).
  • Cluster 9 (1838 observations) has its highest mean marker expression for cancer_epithelial (0.60), followed by cancer_epithelial (0.60).

View imaging_cell:

  • Cluster 0 (303734 observations) has its highest mean marker expression for cancer_epithelial (1.56), followed by cancer_epithelial (1.56).
  • Cluster 1 (185504 observations) has its highest mean marker expression for myeloid (0.48), followed by cancer_epithelial (0.33).
  • Cluster 10 (14923 observations) has its highest mean marker expression for cancer_epithelial (0.70), followed by cancer_epithelial (0.70).
  • Cluster 100 (237 observations) has its highest mean marker expression for cancer_epithelial (0.01), followed by cancer_epithelial (0.01).
  • Cluster 101 (94 observations) has its highest mean marker expression for cancer_epithelial (0.20), followed by cancer_epithelial (0.20).
  • Cluster 102 (35 observations) has its highest mean marker expression for cancer_epithelial (2.39), followed by cancer_epithelial (2.39).
  • Cluster 103 (71 observations) has its highest mean marker expression for cancer_epithelial (0.06), followed by cancer_epithelial (0.06).
  • Cluster 104 (102 observations) has its highest mean marker expression for cancer_epithelial (0.03), followed by cancer_epithelial (0.03).
  • Cluster 105 (84 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 106 (110 observations) has its highest mean marker expression for cancer_epithelial (0.47), followed by cancer_epithelial (0.47).
  • Cluster 107 (70 observations) has its highest mean marker expression for cancer_epithelial (0.63), followed by cancer_epithelial (0.63).
  • Cluster 108 (30 observations) has its highest mean marker expression for myeloid (0.02), followed by cancer_epithelial (0.00).
  • Cluster 109 (390 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 11 (22168 observations) has its highest mean marker expression for cancer_epithelial (0.17), followed by cancer_epithelial (0.17).
  • Cluster 110 (45 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 111 (28 observations) has its highest mean marker expression for myeloid (0.02), followed by cancer_epithelial (0.00).
  • Cluster 112 (280 observations) has its highest mean marker expression for cancer_epithelial (0.01), followed by cancer_epithelial (0.01).
  • Cluster 113 (8 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 114 (18 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 115 (606 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 116 (24 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 117 (84 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 118 (49 observations) has its highest mean marker expression for t_cell (0.02), followed by cancer_epithelial (0.02).
  • Cluster 119 (73 observations) has its highest mean marker expression for cancer_epithelial (0.03), followed by cancer_epithelial (0.03).
  • Cluster 12 (30621 observations) has its highest mean marker expression for cancer_epithelial (0.39), followed by cancer_epithelial (0.39).
  • Cluster 120 (62 observations) has its highest mean marker expression for cancer_epithelial (2.34), followed by cancer_epithelial (2.34).
  • Cluster 121 (37 observations) has its highest mean marker expression for cancer_epithelial (0.06), followed by cancer_epithelial (0.06).
  • Cluster 122 (30 observations) has its highest mean marker expression for cancer_epithelial (2.47), followed by cancer_epithelial (2.47).
  • Cluster 123 (33 observations) has its highest mean marker expression for cancer_epithelial (0.14), followed by cancer_epithelial (0.14).
  • Cluster 124 (338 observations) has its highest mean marker expression for myeloid (0.00), followed by cancer_epithelial (0.00).
  • Cluster 125 (323 observations) has its highest mean marker expression for cancer_epithelial (0.01), followed by cancer_epithelial (0.01).
  • Cluster 126 (105 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 127 (330 observations) has its highest mean marker expression for t_cell (0.00), followed by cancer_epithelial (0.00).
  • Cluster 128 (188 observations) has its highest mean marker expression for fibroblast (2.35), followed by cancer_epithelial (0.00).
  • Cluster 129 (119 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 13 (57920 observations) has its highest mean marker expression for cancer_epithelial (1.23), followed by cancer_epithelial (1.23).
  • Cluster 130 (255 observations) has its highest mean marker expression for cancer_epithelial (0.66), followed by cancer_epithelial (0.66).
  • Cluster 131 (176 observations) has its highest mean marker expression for myeloid (1.53), followed by cancer_epithelial (0.00).
  • Cluster 132 (113 observations) has its highest mean marker expression for cancer_epithelial (0.06), followed by cancer_epithelial (0.06).
  • Cluster 133 (252 observations) has its highest mean marker expression for fibroblast (2.34), followed by cancer_epithelial (0.02).
  • Cluster 134 (309 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 135 (12 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 136 (245 observations) has its highest mean marker expression for t_cell (1.03), followed by cancer_epithelial (0.00).
  • Cluster 137 (113 observations) has its highest mean marker expression for t_cell (1.03), followed by cancer_epithelial (0.00).
  • Cluster 138 (60 observations) has its highest mean marker expression for t_cell (1.62), followed by cancer_epithelial (0.01).
  • Cluster 139 (126 observations) has its highest mean marker expression for t_cell (1.03), followed by cancer_epithelial (0.00).
  • Cluster 14 (33812 observations) has its highest mean marker expression for cancer_epithelial (1.09), followed by cancer_epithelial (1.09).
  • Cluster 140 (162 observations) has its highest mean marker expression for cancer_epithelial (2.13), followed by cancer_epithelial (2.13).
  • Cluster 141 (35 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 142 (32 observations) has its highest mean marker expression for cancer_epithelial (2.44), followed by cancer_epithelial (2.44).
  • Cluster 143 (138 observations) has its highest mean marker expression for cancer_epithelial (0.59), followed by cancer_epithelial (0.59).
  • Cluster 144 (22 observations) has its highest mean marker expression for cancer_epithelial (0.03), followed by cancer_epithelial (0.03).
  • Cluster 145 (26 observations) has its highest mean marker expression for cancer_epithelial (0.08), followed by cancer_epithelial (0.08).
  • Cluster 146 (81 observations) has its highest mean marker expression for b_plasma (0.71), followed by cancer_epithelial (0.00).
  • Cluster 147 (354 observations) has its highest mean marker expression for fibroblast (3.09), followed by cancer_epithelial (0.00).
  • Cluster 148 (39 observations) has its highest mean marker expression for fibroblast (0.04), followed by cancer_epithelial (0.02).
  • Cluster 149 (136 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 15 (30793 observations) has its highest mean marker expression for cancer_epithelial (0.67), followed by cancer_epithelial (0.67).
  • Cluster 150 (240 observations) has its highest mean marker expression for cancer_epithelial (2.44), followed by cancer_epithelial (2.44).
  • Cluster 151 (88 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 152 (54 observations) has its highest mean marker expression for cancer_epithelial (0.68), followed by cancer_epithelial (0.68).
  • Cluster 153 (71 observations) has its highest mean marker expression for b_plasma (0.81), followed by cancer_epithelial (0.03).
  • Cluster 154 (30 observations) has its highest mean marker expression for cancer_epithelial (2.40), followed by cancer_epithelial (2.40).
  • Cluster 155 (549 observations) has its highest mean marker expression for fibroblast (3.09), followed by cancer_epithelial (0.00).
  • Cluster 156 (97 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 157 (54 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 158 (30 observations) has its highest mean marker expression for myeloid (1.22), followed by cancer_epithelial (0.00).
  • Cluster 159 (18 observations) has its highest mean marker expression for fibroblast (2.46), followed by cancer_epithelial (0.00).
  • Cluster 16 (1759 observations) has its highest mean marker expression for cancer_epithelial (0.04), followed by cancer_epithelial (0.04).
  • Cluster 160 (85 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 161 (10 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 162 (1323 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 163 (68 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 164 (13 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 165 (4263 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 166 (40 observations) has its highest mean marker expression for t_cell (1.63), followed by cancer_epithelial (0.00).
  • Cluster 167 (25 observations) has its highest mean marker expression for cancer_epithelial (2.34), followed by cancer_epithelial (2.34).
  • Cluster 168 (163 observations) has its highest mean marker expression for b_plasma (0.81), followed by cancer_epithelial (0.00).
  • Cluster 169 (102 observations) has its highest mean marker expression for b_plasma (0.81), followed by cancer_epithelial (0.00).
  • Cluster 17 (5747 observations) has its highest mean marker expression for cancer_epithelial (0.47), followed by cancer_epithelial (0.47).
  • Cluster 170 (141 observations) has its highest mean marker expression for fibroblast (3.08), followed by cancer_epithelial (0.00).
  • Cluster 171 (170 observations) has its highest mean marker expression for fibroblast (3.09), followed by cancer_epithelial (0.00).
  • Cluster 172 (45 observations) has its highest mean marker expression for fibroblast (2.44), followed by cancer_epithelial (0.00).
  • Cluster 173 (73 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 174 (49 observations) has its highest mean marker expression for cancer_epithelial (0.03), followed by cancer_epithelial (0.03).
  • Cluster 175 (69 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 176 (52 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 177 (115 observations) has its highest mean marker expression for fibroblast (2.44), followed by cancer_epithelial (0.00).
  • Cluster 178 (51 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 179 (147 observations) has its highest mean marker expression for fibroblast (0.01), followed by cancer_epithelial (0.00).
  • Cluster 18 (26651 observations) has its highest mean marker expression for cancer_epithelial (0.37), followed by cancer_epithelial (0.37).
  • Cluster 180 (11 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 181 (39 observations) has its highest mean marker expression for fibroblast (2.44), followed by cancer_epithelial (0.00).
  • Cluster 182 (25 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 183 (526 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 184 (104 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 185 (57 observations) has its highest mean marker expression for fibroblast (2.43), followed by cancer_epithelial (0.00).
  • Cluster 186 (60 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 187 (26 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 188 (77 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 189 (62 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 19 (41414 observations) has its highest mean marker expression for cancer_epithelial (1.04), followed by cancer_epithelial (1.04).
  • Cluster 190 (11445 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 191 (17 observations) has its highest mean marker expression for cancer_epithelial (2.05), followed by cancer_epithelial (2.05).
  • Cluster 192 (10 observations) has its highest mean marker expression for cancer_epithelial (1.94), followed by cancer_epithelial (1.94).
  • Cluster 193 (27 observations) has its highest mean marker expression for fibroblast (2.71), followed by cancer_epithelial (0.00).
  • Cluster 194 (22 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 195 (3005 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 196 (1905 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 197 (29 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 198 (111 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 199 (188 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 2 (195259 observations) has its highest mean marker expression for t_cell (0.62), followed by cancer_epithelial (0.25).
  • Cluster 20 (2491 observations) has its highest mean marker expression for cancer_epithelial (0.39), followed by cancer_epithelial (0.39).
  • Cluster 200 (44 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 201 (45 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 202 (71 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 203 (136 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 204 (56 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 21 (1026 observations) has its highest mean marker expression for cancer_epithelial (1.23), followed by cancer_epithelial (1.23).
  • Cluster 22 (1642 observations) has its highest mean marker expression for cancer_epithelial (1.39), followed by cancer_epithelial (1.39).
  • Cluster 23 (994 observations) has its highest mean marker expression for cancer_epithelial (0.02), followed by cancer_epithelial (0.02).
  • Cluster 24 (1538 observations) has its highest mean marker expression for b_plasma (0.31), followed by cancer_epithelial (0.09).
  • Cluster 25 (780 observations) has its highest mean marker expression for cancer_epithelial (0.32), followed by cancer_epithelial (0.32).
  • Cluster 26 (560 observations) has its highest mean marker expression for cancer_epithelial (0.05), followed by cancer_epithelial (0.05).
  • Cluster 27 (881 observations) has its highest mean marker expression for cancer_epithelial (0.03), followed by cancer_epithelial (0.03).
  • Cluster 28 (961 observations) has its highest mean marker expression for cancer_epithelial (0.15), followed by cancer_epithelial (0.15).
  • Cluster 29 (621 observations) has its highest mean marker expression for cancer_epithelial (2.38), followed by cancer_epithelial (2.38).
  • Cluster 3 (71313 observations) has its highest mean marker expression for b_plasma (0.38), followed by cancer_epithelial (0.11).
  • Cluster 30 (507 observations) has its highest mean marker expression for cancer_epithelial (0.09), followed by cancer_epithelial (0.09).
  • Cluster 31 (1203 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 32 (437 observations) has its highest mean marker expression for cancer_epithelial (0.08), followed by cancer_epithelial (0.08).
  • Cluster 33 (725 observations) has its highest mean marker expression for cancer_epithelial (0.12), followed by cancer_epithelial (0.12).
  • Cluster 34 (1746 observations) has its highest mean marker expression for cancer_epithelial (0.04), followed by cancer_epithelial (0.04).
  • Cluster 35 (484 observations) has its highest mean marker expression for cancer_epithelial (0.84), followed by cancer_epithelial (0.84).
  • Cluster 36 (858 observations) has its highest mean marker expression for cancer_epithelial (0.54), followed by cancer_epithelial (0.54).
  • Cluster 37 (625 observations) has its highest mean marker expression for myeloid (1.49), followed by cancer_epithelial (0.00).
  • Cluster 38 (340 observations) has its highest mean marker expression for cancer_epithelial (0.13), followed by cancer_epithelial (0.13).
  • Cluster 39 (279 observations) has its highest mean marker expression for cancer_epithelial (0.02), followed by cancer_epithelial (0.02).
  • Cluster 4 (116338 observations) has its highest mean marker expression for cancer_epithelial (0.88), followed by cancer_epithelial (0.88).
  • Cluster 40 (1722 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 41 (201 observations) has its highest mean marker expression for cancer_epithelial (0.13), followed by cancer_epithelial (0.13).
  • Cluster 42 (995 observations) has its highest mean marker expression for cancer_epithelial (0.02), followed by cancer_epithelial (0.02).
  • Cluster 43 (957 observations) has its highest mean marker expression for fibroblast (0.00), followed by cancer_epithelial (0.00).
  • Cluster 44 (400 observations) has its highest mean marker expression for cancer_epithelial (0.04), followed by cancer_epithelial (0.04).
  • Cluster 45 (462 observations) has its highest mean marker expression for cancer_epithelial (0.10), followed by cancer_epithelial (0.10).
  • Cluster 46 (149 observations) has its highest mean marker expression for t_cell (0.19), followed by cancer_epithelial (0.00).
  • Cluster 47 (1034 observations) has its highest mean marker expression for cancer_epithelial (0.04), followed by cancer_epithelial (0.04).
  • Cluster 48 (646 observations) has its highest mean marker expression for cancer_epithelial (0.04), followed by cancer_epithelial (0.04).
  • Cluster 49 (228 observations) has its highest mean marker expression for b_plasma (0.07), followed by cancer_epithelial (0.02).
  • Cluster 5 (47618 observations) has its highest mean marker expression for fibroblast (1.01), followed by cancer_epithelial (0.23).
  • Cluster 50 (3853 observations) has its highest mean marker expression for fibroblast (0.00), followed by cancer_epithelial (0.00).
  • Cluster 51 (289 observations) has its highest mean marker expression for myeloid (0.00), followed by cancer_epithelial (0.00).
  • Cluster 52 (322 observations) has its highest mean marker expression for t_cell (0.05), followed by cancer_epithelial (0.03).
  • Cluster 53 (519 observations) has its highest mean marker expression for cancer_epithelial (0.13), followed by cancer_epithelial (0.13).
  • Cluster 54 (254 observations) has its highest mean marker expression for cancer_epithelial (0.02), followed by cancer_epithelial (0.02).
  • Cluster 55 (251 observations) has its highest mean marker expression for cancer_epithelial (0.07), followed by cancer_epithelial (0.07).
  • Cluster 56 (1078 observations) has its highest mean marker expression for cancer_epithelial (0.01), followed by cancer_epithelial (0.01).
  • Cluster 57 (349 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 58 (992 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 59 (128 observations) has its highest mean marker expression for t_cell (0.19), followed by cancer_epithelial (0.05).
  • Cluster 6 (242785 observations) has its highest mean marker expression for cancer_epithelial (1.53), followed by cancer_epithelial (1.53).
  • Cluster 60 (347 observations) has its highest mean marker expression for cancer_epithelial (2.51), followed by cancer_epithelial (2.51).
  • Cluster 61 (312 observations) has its highest mean marker expression for cancer_epithelial (0.46), followed by cancer_epithelial (0.46).
  • Cluster 62 (361 observations) has its highest mean marker expression for t_cell (0.96), followed by cancer_epithelial (0.00).
  • Cluster 63 (184 observations) has its highest mean marker expression for cancer_epithelial (2.84), followed by cancer_epithelial (2.84).
  • Cluster 64 (557 observations) has its highest mean marker expression for cancer_epithelial (0.03), followed by cancer_epithelial (0.03).
  • Cluster 65 (289 observations) has its highest mean marker expression for cancer_epithelial (1.05), followed by cancer_epithelial (1.05).
  • Cluster 66 (831 observations) has its highest mean marker expression for b_plasma (0.00), followed by cancer_epithelial (0.00).
  • Cluster 67 (195 observations) has its highest mean marker expression for cancer_epithelial (0.78), followed by cancer_epithelial (0.78).
  • Cluster 68 (874 observations) has its highest mean marker expression for cancer_epithelial (0.19), followed by cancer_epithelial (0.19).
  • Cluster 69 (900 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 7 (75493 observations) has its highest mean marker expression for b_plasma (0.38), followed by cancer_epithelial (0.24).
  • Cluster 70 (167 observations) has its highest mean marker expression for cancer_epithelial (0.01), followed by cancer_epithelial (0.01).
  • Cluster 71 (416 observations) has its highest mean marker expression for cancer_epithelial (0.02), followed by cancer_epithelial (0.02).
  • Cluster 72 (296 observations) has its highest mean marker expression for cancer_epithelial (1.31), followed by cancer_epithelial (1.31).
  • Cluster 73 (239 observations) has its highest mean marker expression for cancer_epithelial (0.03), followed by cancer_epithelial (0.03).
  • Cluster 74 (69 observations) has its highest mean marker expression for cancer_epithelial (0.13), followed by cancer_epithelial (0.13).
  • Cluster 75 (155 observations) has its highest mean marker expression for t_cell (1.02), followed by cancer_epithelial (0.00).
  • Cluster 76 (253 observations) has its highest mean marker expression for cancer_epithelial (0.03), followed by cancer_epithelial (0.03).
  • Cluster 77 (97 observations) has its highest mean marker expression for cancer_epithelial (2.73), followed by cancer_epithelial (2.73).
  • Cluster 78 (198 observations) has its highest mean marker expression for myeloid (1.20), followed by cancer_epithelial (0.00).
  • Cluster 79 (4177 observations) has its highest mean marker expression for myeloid (0.00), followed by cancer_epithelial (0.00).
  • Cluster 8 (26192 observations) has its highest mean marker expression for cancer_epithelial (0.30), followed by cancer_epithelial (0.30).
  • Cluster 80 (126 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 81 (121 observations) has its highest mean marker expression for cancer_epithelial (0.16), followed by cancer_epithelial (0.16).
  • Cluster 82 (132 observations) has its highest mean marker expression for cancer_epithelial (0.86), followed by cancer_epithelial (0.86).
  • Cluster 83 (276 observations) has its highest mean marker expression for t_cell (1.02), followed by cancer_epithelial (0.00).
  • Cluster 84 (50 observations) has its highest mean marker expression for cancer_epithelial (2.40), followed by cancer_epithelial (2.40).
  • Cluster 85 (249 observations) has its highest mean marker expression for cancer_epithelial (0.01), followed by cancer_epithelial (0.01).
  • Cluster 86 (218 observations) has its highest mean marker expression for fibroblast (0.00), followed by cancer_epithelial (0.00).
  • Cluster 87 (236 observations) has its highest mean marker expression for myeloid (0.00), followed by cancer_epithelial (0.00).
  • Cluster 88 (129 observations) has its highest mean marker expression for cancer_epithelial (0.01), followed by cancer_epithelial (0.01).
  • Cluster 89 (282 observations) has its highest mean marker expression for cancer_epithelial (0.99), followed by cancer_epithelial (0.99).
  • Cluster 9 (35076 observations) has its highest mean marker expression for cancer_epithelial (0.85), followed by cancer_epithelial (0.85).
  • Cluster 90 (525 observations) has its highest mean marker expression for cancer_epithelial (3.08), followed by cancer_epithelial (3.08).
  • Cluster 91 (1089 observations) has its highest mean marker expression for cancer_epithelial (0.01), followed by cancer_epithelial (0.01).
  • Cluster 92 (83 observations) has its highest mean marker expression for cancer_epithelial (0.10), followed by cancer_epithelial (0.10).
  • Cluster 93 (120 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 94 (437 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 95 (91 observations) has its highest mean marker expression for cancer_epithelial (0.02), followed by cancer_epithelial (0.02).
  • Cluster 96 (157 observations) has its highest mean marker expression for b_plasma (1.02), followed by cancer_epithelial (0.05).
  • Cluster 97 (171 observations) has its highest mean marker expression for myeloid (1.25), followed by cancer_epithelial (0.00).
  • Cluster 98 (181 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).
  • Cluster 99 (45 observations) has its highest mean marker expression for cancer_epithelial (0.00), followed by cancer_epithelial (0.00).

Export and reproduce¶

The bundle holds the manifest, the web subsample of each view, the QC and cohort tables and the embeddings.

In [21]:
show_call(
    export_bundle,
    "Writes the manifest, umap.parquet, qc.parquet, cohort.parquet and embedding.h5ad for this cohort. A cohort whose embedding is over 1 GiB gets parts of whole datasets instead (embedding-1.h5ad, embedding-2.h5ad, ...), and anndata.concat over the parts gives the whole cohort.",
)
bundle = export_bundle(
    views,
    cohort,
    qc,
    spec,
    out_dir,
    release,
    runtime_s=time.time() - start,
    mixing=mixing,
    primers_ref=primers_ref,
)
for f in sorted(bundle.iterdir()):
    print(f"{f.relative_to(out_dir)}  {f.stat().st_size} bytes")
manifest = json.loads((bundle / "manifest.json").read_text())
for key in (
    "slug",
    "release",
    "primers_ref",
    "n_datasets",
    "n_papers",
    "technologies",
    "n_cells_before",
    "n_cells_after",
    "n_flagged",
    "path",
    "hardware",
    "runtime_s",
):
    print(f"{key}: {manifest[key]}")

primers.export.export_bundle: Writes the manifest, umap.parquet, qc.parquet, cohort.parquet and embedding.h5ad for this cohort. A cohort whose embedding is over 1 GiB gets parts of whole datasets instead (embedding-1.h5ad, embedding-2.h5ad, ...), and anndata.concat over the parts gives the whole cohort. Source at 4d7af340849c00077f9d21c5b54af9e689a4a070.

ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
human-ovarian-cancer/cohort.parquet  12175 bytes
human-ovarian-cancer/embedding.h5ad  437111753 bytes
human-ovarian-cancer/manifest.json  85254 bytes
human-ovarian-cancer/primer.ipynb  2931853 bytes
human-ovarian-cancer/qc.parquet  11617 bytes
human-ovarian-cancer/umap.parquet  2930970 bytes
slug: human-ovarian-cancer
release: 2026-11-01
primers_ref: 4d7af340849c00077f9d21c5b54af9e689a4a070
n_datasets: 100
n_papers: 16
technologies: ['Visium', 'VisiumHD', 'Xenium']
n_cells_before: 2104735
n_cells_after: 2018057
n_flagged: 2
path: gpu
hardware: NVIDIA B300 SXM6 AC
runtime_s: 898.3
In [22]:
command = (
    f"papermill primers/notebooks/primer.ipynb primer.ipynb -p slug {slug} -p release {release} "
    f"-p primers_ref {primers_ref} -p use_gpu {use_gpu}" + (f" -p smoke {smoke}" if smoke else "")
)
versions = "\n".join(f"  - {k} {v}" for k, v in manifest["versions"].items())
display(
    Markdown(
        f"- Catalog release: `{release}`\n- primers reference: `{primers_ref}`\n- Versions:\n{versions}\n"
        f"- Hardware: {manifest['hardware']} ({platform.system()})\n- Path: {manifest['path']}\n\n"
        "To run this primer again:"
    )
)
show_command(command)
  • Catalog release: 2026-11-01
  • primers reference: 4d7af340849c00077f9d21c5b54af9e689a4a070
  • Versions:
    • spatialdata-db 0.1.3
    • scanpy 1.12.5
    • anndata 0.13.4
    • spatialdata 0.8.0
    • spatialdata-plot 0.4.2
    • rapids-singlecell-cu13 0.18.1
  • Hardware: NVIDIA B300 SXM6 AC (Linux)
  • Path: gpu

To run this primer again:

papermill primers/notebooks/primer.ipynb primer.ipynb -p slug human-ovarian-cancer -p release 2026-11-01 -p primers_ref 4d7af340849c00077f9d21c5b54af9e689a4a070 -p use_gpu True