Use cohorts for machine learning#
This guide goes from a cohort to training-ready data: implement a small TaskAdapter, pull one concatenated
AnnData table out of several stores without downloading images, and loop over the train fold of a
study-level split. It uses four small stores (about 10 MB each):
xVK6T0rajFTgOKci0000: VisiumHD, bone elementcgmLrEw3hgv7yhZf0000: Visium, head of femurgtLUT2zUAH9l9Uzi0002: Visium, mouse lungcdJ4ig3BNiTZMC3T0002: Visium, mouse lung
pip install git+https://github.com/theislab/spatialdata-db-client
import sddb
cat = sddb.Catalog()
cohort = cat.cohort(uids=["xVK6T0rajFTgOKci0000", "cgmLrEw3hgv7yhZf0000", "gtLUT2zUAH9l9Uzi0002", "cdJ4ig3BNiTZMC3T0002"])
cohort
<SpatialDataCohort: 4 datasets>
One table across many stores#
to_anndata(table=...) reads only each member’s tables/<name> group (no images, no shapes) and concatenates
them with an outer join. obs["uid"] says which dataset a row came from and obs["study_id"] which study.
visium = cohort.filter(technology="Visium")
adata = visium.to_anndata(table="table")
print(adata)
adata.obs["uid"].value_counts().reindex([d.uid for d in visium])
AnnData object with n_obs × n_vars = 5662 × 37532
obs: 'in_tissue', 'array_row', 'array_col', 'spot_id', 'region', 'uid', 'study_id'
obsm: 'spatial'
layers: 'csc_counts'
uid
gtLUT2zUAH9l9Uzi0002 459
cdJ4ig3BNiTZMC3T0002 211
cgmLrEw3hgv7yhZf0000 4992
Name: count, dtype: int64
Open stores as SpatialData#
load(elements=[...]) opens every member as SpatialData but copies only the listed elements. sdatas opens
every member completely (download first, so mind the size).
sdatas = visium.load(elements=["tables/table"])
{d.uid: s.tables["table"].shape for d, s in zip(visium, sdatas, strict=True)}
{'gtLUT2zUAH9l9Uzi0002': (459, 19465),
'cdJ4ig3BNiTZMC3T0002': (211, 19465),
'cgmLrEw3hgv7yhZf0000': (4992, 18085)}
full = visium[:1].sdatas[0]
print("images:", len(full.images), "| tables:", list(full.tables))
images: 3 | tables: ['table']
A minimal TaskAdapter#
TaskAdapter is a protocol: the client defines the contract and your task repository implements it. An adapter
declares its name, version and config, states its requirements(), and provides three hooks:
validate_meta(meta): cheap, metadata-only check (advisory), called by the task framework with anObjectMeta(the element inventory, read without any array chunk).validate(sdata): authoritative check on the opened object.build(sdata): produce the task item for one object.
Each hook returns a list of issue strings, empty when all is well (build returns the item).
from typing import Any
from sddb import TaskAdapter
class SpotSummaryAdapter:
"""Per-slide summary: number of spots, genes and total counts."""
name = "spot-summary"
version = "0.1.0"
def __init__(self, min_spots: int = 1_000) -> None:
self.config: dict[str, Any] = {"min_spots": min_spots}
def requirements(self) -> dict[str, Any]:
return {"tables": 1, "elements": ["tables/*"]}
def validate_meta(self, meta) -> list[str]:
return [] if meta.tables else ["no table in store"]
def validate(self, sdata) -> list[str]:
table = next(iter(sdata.tables.values()))
n = self.config["min_spots"]
return [] if table.n_obs >= n else [f"only {table.n_obs} spots (< {n})"]
def build(self, sdata) -> dict[str, float]:
table = next(iter(sdata.tables.values()))
return {"n_spots": table.n_obs, "n_genes": table.n_vars, "total_counts": float(table.X.sum())}
adapter = SpotSummaryAdapter()
isinstance(adapter, TaskAdapter)
True
Loop over a split’s train fold#
split() is group-safe: every study lands in exactly one fold. Datasets without a study_id form their own
group. Each fold is itself a cohort, so you can iterate it, check each object with the adapter and build items. Members keep catalog order throughout this guide, and outputs are keyed by uid.
folds = cohort.split(by="study_id", train=0.5, val=0.25, test=0.25, seed=0)
{name: [d.uid for d in fold] for name, fold in folds.items()}
{'train': ['cdJ4ig3BNiTZMC3T0002', 'cgmLrEw3hgv7yhZf0000'],
'val': ['xVK6T0rajFTgOKci0000'],
'test': ['gtLUT2zUAH9l9Uzi0002']}
items = {}
for ds in folds["train"]:
table = ds.default_table or "table"
sdata = ds.load(lazy=False, elements=[f"tables/{table}"])
issues = adapter.validate(sdata)
if issues:
print(ds.uid, "skipped:", issues)
continue
items[ds.uid] = adapter.build(sdata)
items
cdJ4ig3BNiTZMC3T0002 skipped: ['only 211 spots (< 1000)']
{'cgmLrEw3hgv7yhZf0000': {'n_spots': 4992,
'n_genes': 18085,
'total_counts': 1069313.0}}
Because the split manifest travels with the fold, you can store it next to your results and recover the exact assignment later (see the cohorts guide).
folds["train"].split_manifest.assignments
{'gtLUT2zUAH9l9Uzi0002': 'test',
'cdJ4ig3BNiTZMC3T0002': 'train',
'cgmLrEw3hgv7yhZf0000': 'train',
'xVK6T0rajFTgOKci0000': 'val'}
print("Catalog generated at:", cat.generated_at)
Catalog generated at: 2026-10-18