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-kidney"
release = "2026-11-01"
out_dir = "bundles"
primers_ref = "4d7af340849c00077f9d21c5b54af9e689a4a070"
dev_note = ""

Human kidney in health and disease¶

Cohort human-kidney, 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-kidney
query: {'organism': 'human', 'tissue': 'kidney', 'n_obs__lte': 250000}
include_uids: None
exclude_uids: ['QhWLkUPIwgReWxUl0000', '7cvm04SitwDiUccp0000', 'GrL6vmD9cQIX9kWy0000', '0hvcEnilQM3UbBEX0000', 'TPUEOPBx6Bl9XSwV0000', 'RxMxBceGIVY7cunl0000', '9ECHPci2ByJEXpmm0000', 'xR5R5oIEI5vC5rw40000', 'jVoLDQ0g9HrFR5090000', 'L63qBPEi1Jb14kUM0000', 'GIvMm2g196WM29FC0000', 'WUuICN3yJz9v0cHY0000', 'xtDuriWbektArUTi0000', 'sVYWy3ftQNgttMRP0000', 'H7X7fQqJO0v9XX8T0000', 'NHyd5DFysygNrkdu0000', 'kZEVJcHN2Kwfs9W70000', 'sGZxtkw1WXUZ7YFq0000']
families: ['spot', 'imaging']
marker_set: kidney
rationale: Diabetic, glomerular and cancer kidney on Visium, Visium HD and Xenium across many labs; the cap leaves out the largest Xenium and HD sections, and a 50-gene Xenium series is left out.
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 M7zdYIvV1fmS9H9i0000 GSM7974887 Visium spot 10.1158/2159-8290.CD-24-0187 doi ncbi-geo-policy 2258 kept
1 42EQMGTLwzdPRPKw0000 GSM7974477 Visium spot 10.1038/s41467-024-52525-w doi ncbi-geo-policy 388 kept
2 D4I13ftxuL8b4qjP0000 human-kidney-11-mm-capture-area-ffpe-2-standard Visium spot 10x Genomics demo datasets 10x_demo CC-BY-4.0 5936 kept
3 Fwp5w6FPox2J8RtV0000 GSM9210736 Xenium imaging_cell 10.1016/j.celrep.2026.117483 doi ncbi-geo-policy 34980 kept
4 jGRcwO1RMUByhiGB0000 GSM7115963 Visium spot geo-gse228169 geo_series ncbi-geo-policy 3555 kept
... ... ... ... ... ... ... ... ... ...
152 VuiDeCXkuqd9M6B80000 GSM9844158 Xenium imaging_cell n/a none ncbi-geo-policy 13860 dropped: no paper (no study_id, series or collection)
153 Oe8QcqIOUO91Mkct0000 GSM9844164 Xenium imaging_cell n/a none ncbi-geo-policy 98714 dropped: no paper (no study_id, series or collection)
154 F6vuWevo1Qa8i2ob0000 GSM9844157 Xenium imaging_cell n/a none ncbi-geo-policy 23409 dropped: no paper (no study_id, series or collection)
155 EKqdvxicsy7bBSr70000 GSM9844156 Xenium imaging_cell n/a none ncbi-geo-policy 121864 dropped: no paper (no study_id, series or collection)
156 UwbrdvEmOWDDgSC30000 GSM9844159 Xenium imaging_cell n/a none ncbi-geo-policy 16807 dropped: no paper (no study_id, series or collection)

157 rows × 9 columns

status members
0 kept 144
1 dropped: no paper (no study_id, series or collection) 13
144 kept, 13 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 0Y6QewKoDCORRZiZ0000 Visium 461 458 6550.0 3362.0 7.878 False kept spot
1 2Px4hjJFRq22eikg0000 Visium 2802 2798 15591.0 6647.0 2.342 False kept spot
2 2YwSMyV1o6mZlqSR0000 Visium 370 370 11169.5 4476.5 7.254 False kept spot
3 36rv58XjdrtRaHrW0000 Visium 1949 1946 17882.0 5285.0 4.446 False kept spot
4 42EQMGTLwzdPRPKw0000 Visium 388 380 13409.0 5748.0 0.0 False kept spot
... ... ... ... ... ... ... ... ... ... ... ...
139 mhUVgmN7Np9ERjpD0000 Xenium 54143 51796 56.0 30.0 n/a False kept imaging_cell
140 mv6fU7wRDHoMkjEc0000 Xenium 26648 25811 73.0 38.0 n/a False kept imaging_cell
141 oH98OgEjk6P33h620000 Xenium 44621 43365 69.0 37.0 n/a False kept imaging_cell
142 qe9O7bVorvQTCEg00000 Xenium 44598 43575 88.0 44.0 n/a False kept imaging_cell
143 vllMYzmHKoQQHzZ50000 Xenium 32584 25923 26.0 16.0 n/a True median counts 26 < 30 kept imaging_cell

144 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: 124 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 'kidney', cited from 10.1038/s41586-023-05769-3, 10.1126/science.aar2131.
markers
compartment
proximal_tubule LRP2, CUBN, SLC34A1
thick_ascending_limb UMOD, SLC12A1
distal_tubule SLC12A3, CALB1
collecting_duct_principal AQP2, AQP3
collecting_duct_intercalated ATP6V1B1, SLC4A1
podocyte NPHS1, NPHS2, WT1
endothelial PECAM1, EMCN, PLVAP
fibroblast DCN, COL1A1, PDGFRB
immune PTPRC, CD68, CD3E
panel compartment genes note
0 Xenium-5092-381adb2d collecting_duct_principal SCNN1G, HSD11B2 principal-cell genes on this panel; GATA3 left out (also in distal segments)
1 Xenium-377-9193d582 proximal_tubule SLC22A8 OAT3, proximal-tubule transporter on this panel
2 Xenium-377-9193d582 distal_tubule PVALB distal convoluted tubule gene on this panel
3 Xenium-377-9193d582 podocyte not measured no podocyte gene 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

Visium, section xBonfFXEt2OTpaQP0000 (1349 observations).

2026-11-04 05:31:02 | [INFO] root_attr: version
2026-11-04 05:31:02 | [INFO] root_attr: multiscales
2026-11-04 05:31:02 | [INFO] root_attr: omero
2026-11-04 05:31:02 | [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 05:31:03 | [INFO] resolution: s0
2026-11-04 05:31:03 | [INFO]  - shape ('c', 'y', 'x') = (3, 600, 527)
2026-11-04 05:31:03 | [INFO]  - chunks =  ['3', '600', '527']
2026-11-04 05:31:03 | [INFO]  - dtype = uint8
2026-11-04 05:31:03 | [INFO] root_attr: version
2026-11-04 05:31:03 | [INFO] root_attr: multiscales
2026-11-04 05:31:03 | [INFO] root_attr: omero

VisiumHD, section bv10bJQZ5klI5nMu0000 (154937 observations).

Xenium, section qe9O7bVorvQTCEg00000 (44598 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: 276312 observations, 124 sections; 2000 highly variable genes of the 16849 measured, expressed genes (seurat_v3 on raw counts, section as batch); its 7 gene panels have 17,943 to 37,082 genes each and share 16,851.
imaging_cell: 878788 observations, 17 sections; all 129 measured, expressed genes of the view; its 3 gene panels have 377 to 5,092 genes each and share 129. only 129 shared genes (under 200)
member 4aa7meIGBU5btyG60000 dropped: panel shares 7 genes with the rest of its technology
member F5aA0qoFwU5KqSQP0000 dropped: panel shares 7 genes with the rest of its technology
member E5f35NJ4oQ4NGo2J0000 dropped: panel shares 7 genes with the rest of its technology
Xenium panel of AW1AUSyQ5HJuvJUp0000: kept: shares 129 of 224 shared genes
Xenium panel of USOaqAveOg9ScruM0001, RwEKt4wbzJsqG4yK0001, W1bZvhwcfHpd4PID0000: kept: shares 129 of 198 shared genes
Joint view: no. fewer than two families keep a technology
  left out Visium (gate3): loses 99% of its observations below 500 counts on the 124 shared genes (limit 50%)
  left out VisiumHD (gate3): loses 100% of its observations below 200 counts on the 124 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.128 0.897 1.277
1 spot section before Harmony other_share 0.195 0.944 0.977
2 spot section after Harmony entropy 0.57 0.897 1.277
3 spot section after Harmony other_share 0.729 0.944 0.977
4 spot paper before Harmony entropy 0.063 0.73 0.835
5 spot paper before Harmony other_share 0.097 0.854 0.883
6 spot paper after Harmony entropy 0.369 0.729 0.835
7 spot paper after Harmony other_share 0.532 0.854 0.883
8 imaging_cell section before Harmony entropy 0.515 0.823 0.931
9 imaging_cell section before Harmony other_share 0.63 0.88 0.909
10 imaging_cell section after Harmony entropy 0.551 0.823 0.931
11 imaging_cell section after Harmony other_share 0.651 0.88 0.909
12 imaging_cell paper before Harmony entropy 0.325 0.725 0.766
13 imaging_cell paper before Harmony other_share 0.265 0.566 0.586
14 imaging_cell paper after Harmony entropy 0.387 0.728 0.766
15 imaging_cell paper after Harmony other_share 0.313 0.567 0.586
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: 11 clusters on nn_harmony, 29 marker genes.

No description has been provided for this image
observations top compartment top mean runner-up runner-up mean
cluster
0 15896 fibroblast 0.951 collecting_duct_principal 0.575
1 44259 fibroblast 1.314 collecting_duct_principal 0.129
10 56689 fibroblast 0.757 collecting_duct_principal 0.111
2 13330 fibroblast 1.001 collecting_duct_principal 0.127
3 26201 fibroblast 0.492 collecting_duct_principal 0.342
4 13600 podocyte 0.999 collecting_duct_principal 0.368
5 4511 proximal_tubule 0.672 collecting_duct_principal 0.243
6 22614 thick_ascending_limb 1.485 collecting_duct_principal 0.634
7 52754 proximal_tubule 1.011 collecting_duct_principal 0.473
8 19488 collecting_duct_principal 1.411 collecting_duct_principal 1.411
9 6970 distal_tubule 1.662 collecting_duct_principal 0.555

View imaging_cell: 176 clusters on nn_harmony, 5 marker genes.

No description has been provided for this image
observations top compartment top mean runner-up runner-up mean
cluster
0 122964 endothelial 0.111 endothelial 0.111
1 158136 proximal_tubule 0.498 endothelial 0.032
10 21461 thick_ascending_limb 0.155 endothelial 0.058
100 19 proximal_tubule 0.0 endothelial 0.0
101 26 proximal_tubule 0.0 endothelial 0.0
... ... ... ... ... ...
95 14 proximal_tubule 0.0 endothelial 0.0
96 747 proximal_tubule 0.0 endothelial 0.0
97 84 proximal_tubule 0.0 endothelial 0.0
98 9 proximal_tubule 0.0 endothelial 0.0
99 21 proximal_tubule 0.0 endothelial 0.0

176 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 (15896 observations) has its highest mean marker expression for fibroblast (0.95), followed by collecting_duct_principal (0.57).
  • Cluster 1 (44259 observations) has its highest mean marker expression for fibroblast (1.31), followed by collecting_duct_principal (0.13).
  • Cluster 10 (56689 observations) has its highest mean marker expression for fibroblast (0.76), followed by collecting_duct_principal (0.11).
  • Cluster 2 (13330 observations) has its highest mean marker expression for fibroblast (1.00), followed by collecting_duct_principal (0.13).
  • Cluster 3 (26201 observations) has its highest mean marker expression for fibroblast (0.49), followed by collecting_duct_principal (0.34).
  • Cluster 4 (13600 observations) has its highest mean marker expression for podocyte (1.00), followed by collecting_duct_principal (0.37).
  • Cluster 5 (4511 observations) has its highest mean marker expression for proximal_tubule (0.67), followed by collecting_duct_principal (0.24).
  • Cluster 6 (22614 observations) has its highest mean marker expression for thick_ascending_limb (1.49), followed by collecting_duct_principal (0.63).
  • Cluster 7 (52754 observations) has its highest mean marker expression for proximal_tubule (1.01), followed by collecting_duct_principal (0.47).
  • Cluster 8 (19488 observations) has its highest mean marker expression for collecting_duct_principal (1.41), followed by collecting_duct_principal (1.41).
  • Cluster 9 (6970 observations) has its highest mean marker expression for distal_tubule (1.66), followed by collecting_duct_principal (0.56).

View imaging_cell:

  • Cluster 0 (122964 observations) has its highest mean marker expression for endothelial (0.11), followed by endothelial (0.11).
  • Cluster 1 (158136 observations) has its highest mean marker expression for proximal_tubule (0.50), followed by endothelial (0.03).
  • Cluster 10 (21461 observations) has its highest mean marker expression for thick_ascending_limb (0.15), followed by endothelial (0.06).
  • Cluster 100 (19 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 101 (26 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 102 (16 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 103 (288 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 104 (29 observations) has its highest mean marker expression for proximal_tubule (2.13), followed by endothelial (0.00).
  • Cluster 105 (50 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 106 (426 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 107 (38 observations) has its highest mean marker expression for endothelial (0.02), followed by endothelial (0.02).
  • Cluster 108 (48 observations) has its highest mean marker expression for thick_ascending_limb (2.69), followed by endothelial (0.00).
  • Cluster 109 (35 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 11 (15975 observations) has its highest mean marker expression for endothelial (0.45), followed by endothelial (0.45).
  • Cluster 110 (1300 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 111 (39 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 112 (27 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 113 (13 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 114 (48 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 115 (130 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 116 (44 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 117 (98 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 118 (69 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 119 (15 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 12 (13871 observations) has its highest mean marker expression for thick_ascending_limb (0.06), followed by endothelial (0.04).
  • Cluster 120 (30 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 121 (23 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 122 (124 observations) has its highest mean marker expression for proximal_tubule (0.94), followed by endothelial (0.00).
  • Cluster 123 (110 observations) has its highest mean marker expression for endothelial (0.01), followed by endothelial (0.01).
  • Cluster 124 (39 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 125 (33 observations) has its highest mean marker expression for proximal_tubule (0.05), followed by endothelial (0.00).
  • Cluster 126 (22 observations) has its highest mean marker expression for proximal_tubule (2.08), followed by endothelial (0.00).
  • Cluster 127 (30 observations) has its highest mean marker expression for proximal_tubule (2.71), followed by endothelial (0.00).
  • Cluster 128 (32 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 129 (14 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 13 (812 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.01).
  • Cluster 130 (197 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 131 (11 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 132 (7 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 133 (30 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 134 (21 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 135 (79 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 136 (42 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 137 (44 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 138 (56 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 139 (27 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 14 (2015 observations) has its highest mean marker expression for endothelial (0.04), followed by endothelial (0.04).
  • Cluster 140 (51 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 141 (171 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 142 (42 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 143 (45 observations) has its highest mean marker expression for proximal_tubule (2.08), followed by endothelial (0.00).
  • Cluster 144 (192 observations) has its highest mean marker expression for endothelial (2.70), followed by endothelial (2.70).
  • Cluster 145 (54 observations) has its highest mean marker expression for endothelial (2.71), followed by endothelial (2.71).
  • Cluster 146 (39 observations) has its highest mean marker expression for proximal_tubule (1.73), followed by endothelial (0.00).
  • Cluster 147 (62 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 148 (38 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 149 (124 observations) has its highest mean marker expression for proximal_tubule (2.07), followed by endothelial (0.00).
  • Cluster 15 (741 observations) has its highest mean marker expression for thick_ascending_limb (0.03), followed by endothelial (0.03).
  • Cluster 150 (181 observations) has its highest mean marker expression for proximal_tubule (2.71), followed by endothelial (0.00).
  • Cluster 151 (204 observations) has its highest mean marker expression for thick_ascending_limb (2.71), followed by endothelial (0.00).
  • Cluster 152 (67 observations) has its highest mean marker expression for thick_ascending_limb (2.71), followed by endothelial (0.00).
  • Cluster 153 (21 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 154 (17 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 155 (16 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 156 (15 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 157 (52 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 158 (22 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 159 (306 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 16 (700 observations) has its highest mean marker expression for proximal_tubule (0.05), followed by endothelial (0.03).
  • Cluster 160 (182 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 161 (55 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 162 (41 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 163 (1415 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 164 (6509 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 165 (420 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 166 (344 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 167 (213 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 168 (501 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 169 (76 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 17 (887 observations) has its highest mean marker expression for endothelial (0.03), followed by endothelial (0.03).
  • Cluster 170 (73 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 171 (101 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 172 (32 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 173 (111 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 174 (32 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 175 (46 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 18 (807 observations) has its highest mean marker expression for endothelial (0.00), followed by endothelial (0.00).
  • Cluster 19 (630 observations) has its highest mean marker expression for proximal_tubule (0.01), followed by endothelial (0.01).
  • Cluster 2 (76767 observations) has its highest mean marker expression for endothelial (0.98), followed by endothelial (0.98).
  • Cluster 20 (588 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.00).
  • Cluster 21 (728 observations) has its highest mean marker expression for endothelial (0.02), followed by endothelial (0.02).
  • Cluster 22 (302 observations) has its highest mean marker expression for endothelial (0.01), followed by endothelial (0.01).
  • Cluster 23 (1897 observations) has its highest mean marker expression for proximal_tubule (0.02), followed by endothelial (0.01).
  • Cluster 24 (433 observations) has its highest mean marker expression for endothelial (0.01), followed by endothelial (0.01).
  • Cluster 25 (425 observations) has its highest mean marker expression for proximal_tubule (0.01), followed by endothelial (0.00).
  • Cluster 26 (440 observations) has its highest mean marker expression for endothelial (0.01), followed by endothelial (0.01).
  • Cluster 27 (344 observations) has its highest mean marker expression for proximal_tubule (0.02), followed by endothelial (0.00).
  • Cluster 28 (511 observations) has its highest mean marker expression for endothelial (0.00), followed by endothelial (0.00).
  • Cluster 29 (1145 observations) has its highest mean marker expression for endothelial (0.02), followed by endothelial (0.02).
  • Cluster 3 (72853 observations) has its highest mean marker expression for thick_ascending_limb (0.07), followed by endothelial (0.03).
  • Cluster 30 (289 observations) has its highest mean marker expression for endothelial (1.16), followed by endothelial (1.16).
  • Cluster 31 (334 observations) has its highest mean marker expression for proximal_tubule (0.01), followed by endothelial (0.01).
  • Cluster 32 (1218 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 33 (314 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.00).
  • Cluster 34 (964 observations) has its highest mean marker expression for endothelial (0.00), followed by endothelial (0.00).
  • Cluster 35 (329 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 36 (176 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 37 (238 observations) has its highest mean marker expression for thick_ascending_limb (0.02), followed by endothelial (0.00).
  • Cluster 38 (590 observations) has its highest mean marker expression for proximal_tubule (0.30), followed by endothelial (0.00).
  • Cluster 39 (170 observations) has its highest mean marker expression for thick_ascending_limb (0.04), followed by endothelial (0.02).
  • Cluster 4 (84834 observations) has its highest mean marker expression for immune (0.41), followed by endothelial (0.17).
  • Cluster 40 (478 observations) has its highest mean marker expression for thick_ascending_limb (0.00), followed by endothelial (0.00).
  • Cluster 41 (786 observations) has its highest mean marker expression for endothelial (0.00), followed by endothelial (0.00).
  • Cluster 42 (126 observations) has its highest mean marker expression for proximal_tubule (0.01), followed by endothelial (0.00).
  • Cluster 43 (318 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 44 (323 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.00).
  • Cluster 45 (70 observations) has its highest mean marker expression for thick_ascending_limb (0.11), followed by endothelial (0.00).
  • Cluster 46 (169 observations) has its highest mean marker expression for endothelial (0.00), followed by endothelial (0.00).
  • Cluster 47 (97 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 48 (153 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 49 (185 observations) has its highest mean marker expression for thick_ascending_limb (0.02), followed by endothelial (0.01).
  • Cluster 5 (81364 observations) has its highest mean marker expression for endothelial (0.27), followed by endothelial (0.27).
  • Cluster 50 (129 observations) has its highest mean marker expression for collecting_duct_intercalated (0.01), followed by endothelial (0.00).
  • Cluster 51 (146 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 52 (193 observations) has its highest mean marker expression for endothelial (0.09), followed by endothelial (0.09).
  • Cluster 53 (334 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.00).
  • Cluster 54 (189 observations) has its highest mean marker expression for endothelial (0.04), followed by endothelial (0.04).
  • Cluster 55 (151 observations) has its highest mean marker expression for immune (0.03), followed by endothelial (0.02).
  • Cluster 56 (184 observations) has its highest mean marker expression for immune (0.00), followed by endothelial (0.00).
  • Cluster 57 (91 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.01).
  • Cluster 58 (215 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.00).
  • Cluster 59 (692 observations) has its highest mean marker expression for thick_ascending_limb (0.00), followed by endothelial (0.00).
  • Cluster 6 (48820 observations) has its highest mean marker expression for thick_ascending_limb (1.84), followed by endothelial (0.02).
  • Cluster 60 (236 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 61 (37 observations) has its highest mean marker expression for thick_ascending_limb (0.02), followed by endothelial (0.01).
  • Cluster 62 (48 observations) has its highest mean marker expression for thick_ascending_limb (0.09), followed by endothelial (0.02).
  • Cluster 63 (78 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 64 (130 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 65 (125 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.00).
  • Cluster 66 (115 observations) has its highest mean marker expression for endothelial (0.03), followed by endothelial (0.03).
  • Cluster 67 (768 observations) has its highest mean marker expression for thick_ascending_limb (0.00), followed by endothelial (0.00).
  • Cluster 68 (38 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 69 (250 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 7 (94883 observations) has its highest mean marker expression for collecting_duct_intercalated (0.36), followed by endothelial (0.03).
  • Cluster 70 (108 observations) has its highest mean marker expression for endothelial (0.01), followed by endothelial (0.01).
  • Cluster 71 (229 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 72 (297 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 73 (140 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 74 (479 observations) has its highest mean marker expression for endothelial (0.01), followed by endothelial (0.01).
  • Cluster 75 (123 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 76 (61 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 77 (94 observations) has its highest mean marker expression for proximal_tubule (0.02), followed by endothelial (0.00).
  • Cluster 78 (88 observations) has its highest mean marker expression for endothelial (0.01), followed by endothelial (0.01).
  • Cluster 79 (62 observations) has its highest mean marker expression for immune (0.01), followed by endothelial (0.00).
  • Cluster 8 (30819 observations) has its highest mean marker expression for endothelial (0.31), followed by endothelial (0.31).
  • Cluster 80 (148 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 81 (321 observations) has its highest mean marker expression for thick_ascending_limb (0.00), followed by endothelial (0.00).
  • Cluster 82 (44 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 83 (146 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.00).
  • Cluster 84 (98 observations) has its highest mean marker expression for proximal_tubule (0.02), followed by endothelial (0.00).
  • Cluster 85 (90 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 86 (69 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 87 (245 observations) has its highest mean marker expression for thick_ascending_limb (0.00), followed by endothelial (0.00).
  • Cluster 88 (88 observations) has its highest mean marker expression for proximal_tubule (0.01), followed by endothelial (0.00).
  • Cluster 89 (144 observations) has its highest mean marker expression for proximal_tubule (0.01), followed by endothelial (0.00).
  • Cluster 9 (11264 observations) has its highest mean marker expression for endothelial (0.03), followed by endothelial (0.03).
  • Cluster 90 (109 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 91 (26 observations) has its highest mean marker expression for proximal_tubule (2.45), followed by endothelial (0.00).
  • Cluster 92 (159 observations) has its highest mean marker expression for thick_ascending_limb (0.01), followed by endothelial (0.00).
  • Cluster 93 (20 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 94 (34 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 95 (14 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 96 (747 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 97 (84 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 98 (9 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (0.00).
  • Cluster 99 (21 observations) has its highest mean marker expression for proximal_tubule (0.00), followed by endothelial (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.
ImplicitModificationWarning: Trying to modify attribute `.obs` of view, initializing view as actual.
human-kidney/cohort.parquet  14161 bytes
human-kidney/embedding.h5ad  253642183 bytes
human-kidney/manifest.json  114664 bytes
human-kidney/primer.ipynb  2629802 bytes
human-kidney/qc.parquet  13711 bytes
human-kidney/umap.parquet  2895692 bytes
slug: human-kidney
release: 2026-11-01
primers_ref: 4d7af340849c00077f9d21c5b54af9e689a4a070
n_datasets: 141
n_papers: 23
technologies: ['Visium', 'VisiumHD', 'Xenium']
n_cells_before: 1616617
n_cells_after: 1172134
n_flagged: 18
path: gpu
hardware: NVIDIA B300 SXM6 AC
runtime_s: 731.9
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-kidney -p release 2026-11-01 -p primers_ref 4d7af340849c00077f9d21c5b54af9e689a4a070 -p use_gpu True