Curate and ingest spatial data
¶
Now that we’ve analyzed and visualized the example dataset in the previous notebooks, let’s learn how to curate and ingest our own spatial data.
import lamindb as ln
import bionty as bt
import spatialdata as sd
ln.track()
Show code cell output
→ connected lamindb: testuser1/test-spatial
→ created Transform('52HTebx27nlA0000', key='spatial3.ipynb'), started new Run('jWpvdszhKhwUCWWI') at 2026-10-03 08:37:00 UTC
• tip: to identify the notebook across renames, pass the uid: ln.track("52HTebx27nlA")
Creating artifacts¶
You can use from_spatialdata() method to create an Artifact object from a SpatialData object.
example_blobs_sdata = ln.core.datasets.spatialdata_blobs()
example_blobs_sdata
Show code cell output
SpatialData object
├── Images
│ ├── 'blobs_image': DataArray[cyx] (3, 512, 512)
│ └── 'blobs_multiscale_image': DataTree[cyx] (3, 512, 512), (3, 256, 256), (3, 128, 128)
├── Labels
│ ├── 'blobs_labels': DataArray[yx] (512, 512)
│ └── 'blobs_multiscale_labels': DataTree[yx] (512, 512), (256, 256), (128, 128)
├── Points
│ └── 'blobs_points': DataFrame with shape: (<Delayed>, 4) (2D points)
├── Shapes
│ ├── 'blobs_circles': GeoDataFrame shape: (5, 2) (2D shapes)
│ ├── 'blobs_multipolygons': GeoDataFrame shape: (2, 1) (2D shapes)
│ └── 'blobs_polygons': GeoDataFrame shape: (5, 1) (2D shapes)
└── Tables
└── 'table': AnnData (26, 3)
with coordinate systems:
▸ 'global', with elements:
blobs_image (Images), blobs_multiscale_image (Images), blobs_labels (Labels), blobs_multiscale_labels (Labels), blobs_points (Points), blobs_circles (Shapes), blobs_multipolygons (Shapes), blobs_polygons (Shapes)
blobs_af = ln.Artifact.from_spatialdata(
example_blobs_sdata, key="example_blobs.zarr"
).save()
blobs_af
Show code cell output
Artifact(uid='ysggpUJW75NJevhA0000', key='example_blobs.zarr', description=None, suffix='.zarr', kind='dataset', otype='SpatialData', size=13075473, hash='MBM3kDi-GI4rzjXg5_eOag', n_files=80, n_observations=None, extra_data=None, branch_id=1, created_on_id=1, space_id=1, storage_id=1, run_id=3, schema_id=None, created_by_id=1, created_at=2026-10-03 08:37:06 UTC, is_locked=False, version_tag=None, is_latest=True)
To retrieve the object back from the database you can, e.g., query by key:
example_blobs_sdata = ln.Artifact.get(key="example_blobs.zarr")
local_zarr_path = blobs_af.cache() # returns a local path to the cached .zarr store
example_blobs_sdata = (
blobs_af.load() # calls sd.read_zarr() on a locally cached .zarr store
)
To see data lineage:
blobs_af.view_lineage()
Show code cell output
Curating artifacts¶
For the remainder of the guide, we will work with two 10X Xenium and a 10X Visium H&E image datasets that were ingested in raw form here.
Metadata is stored in two places in the SpatialData object:
Dataset level metadata is stored in
sdata.attrs["sample"].Measurement specific metadata is stored in the associated tables in
sdata.tables.
Define a schema¶
We define a lamindb.Schema to curate both sample and table metadata.
Curating different spatial technologies
Reading different spatial technologies into SpatialData objects can result in very different objects with different metadata. Therefore, it can be useful to define technology specific Schemas by reusing Schema components.
# define features
ln.Feature(name="organism", dtype=bt.Organism).save()
ln.Feature(name="assay", dtype=bt.ExperimentalFactor).save()
ln.Feature(name="disease", dtype=bt.Disease).save()
ln.Feature(name="tissue", dtype=bt.Tissue).save()
ln.Feature(name="celltype_major", dtype=bt.CellType).save()
# define simple schemas
flexible_metadata_schema = ln.Schema(
name="Flexible metadata", itype=ln.Feature, coerce_dtype=True
).save()
ensembl_gene_ids = ln.Schema(
name="Spatial var level (Ensembl gene id)", itype=bt.Gene.ensembl_gene_id
).save()
# define composite schema
spatial_schema = ln.Schema(
name="Spatialdata schema (flexible)",
otype="SpatialData",
slots={
"attrs:sample": flexible_metadata_schema,
"tables:table:obs": flexible_metadata_schema,
"tables:table:var.T": ensembl_gene_ids,
},
).save()
Show code cell output
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map feature into a type hierarchy
! tip: pass `type` to map schema into a type hierarchy
! tip: pass `type` to map schema into a type hierarchy
! tip: pass `type` to map schema into a type hierarchy
/tmp/ipykernel_3098/2821844419.py:9: DeprecationWarning: `coerce_dtype` argument was renamed to `coerce` and will be removed in a future release.
flexible_metadata_schema = ln.Schema(
Curate a Xenium dataset¶
Create the central query object of our public lamindata instance:
db = ln.DB("laminlabs/lamindata")
Show code cell output
• tip: to work with the additional module (pertdb) of database laminlabs/lamindata, configure your environment for it: lamin settings modules set bionty,pertdb
# load first of two cropped Xenium datasets
xenium_aligned_1_sdata = db.Artifact.get(key="xenium_aligned_1_guide_min.zarr").load()
xenium_aligned_1_sdata
Show code cell output
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/lamindb/core/storage/_zarr.py:119: UserWarning: SpatialData is not stored in the most current format. If you want to use Zarr v3, please write the store to a new location using `sdata.write()`.
scverse_obj = with_package("spatialdata", lambda mod: mod.read_zarr(store))
→ Artifact xenium_aligned_1_guide_min.zarr: 2 transferred, 0 already on target
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/zarr/core/group.py:3413: ZarrUserWarning: Object at zmetadata is not recognized as a component of a Zarr hierarchy.
warnings.warn(
SpatialData object, with associated Zarr store: /home/runner/.cache/lamindb/lamindata/xenium_aligned_1_guide_min.zarr
├── Images
│ ├── 'morphology_focus': DataTree[cyx] (1, 2310, 3027), (1, 1155, 1514), (1, 578, 757), (1, 288, 379), (1, 145, 189)
│ └── 'morphology_mip': DataTree[cyx] (1, 2310, 3027), (1, 1155, 1514), (1, 578, 757), (1, 288, 379), (1, 145, 189)
├── Points
│ └── 'transcripts': DataFrame with shape: (415653, 8) (3D points)
├── Shapes
│ ├── 'cell_boundaries': GeoDataFrame shape: (1899, 1) (2D shapes)
│ └── 'cell_circles': GeoDataFrame shape: (1812, 2) (2D shapes)
└── Tables
└── 'table': AnnData (1812, 313)
with coordinate systems:
▸ 'aligned', with elements:
morphology_focus (Images), morphology_mip (Images), transcripts (Points), cell_boundaries (Shapes), cell_circles (Shapes)
▸ 'global', with elements:
morphology_focus (Images), morphology_mip (Images), transcripts (Points), cell_boundaries (Shapes), cell_circles (Shapes)
xenium_curator = ln.curators.SpatialDataCurator(xenium_aligned_1_sdata, spatial_schema)
try:
xenium_curator.validate()
except ln.errors.ValidationError as error:
print(error)
Show code cell output
9 terms not validated in feature 'celltype_major' in slot 'tables:table:obs': 'CAFs', 'Endothelial', 'Myeloid', 'PVL', 'T-cells', 'B-cells', 'Normal Epithelial', 'Plasmablasts', 'Cancer Epithelial'
→ fix typos, remove non-existent values, or create objects via:
objects = bionty.CellType.from_values(['CAFs', 'Endothelial', 'Myeloid', 'PVL', 'T-cells', 'B-cells', 'Normal Epithelial', 'Plasmablasts', 'Cancer Epithelial'], field='name').save()
celltype_mapping = {
"CAFs": "cancer associated fibroblast",
"Endothelial": "endothelial cell",
"Myeloid": "myeloid cell",
"PVL": "perivascular cell",
"T-cells": "T cell",
"B-cells": "B cell",
"Normal Epithelial": "epithelial cell",
"Plasmablasts": "plasmablast",
"Cancer Epithelial": "neoplastic epithelial cell",
}
xenium_aligned_1_sdata.tables["table"].obs["celltype_major"] = (
xenium_aligned_1_sdata.tables["table"]
.obs["celltype_major"]
.cat.rename_categories(celltype_mapping)
)
try:
xenium_curator.validate()
except ln.errors.ValidationError as error:
print(error)
Show code cell output
2 terms not validated in feature 'celltype_major' in slot 'tables:table:obs': 'cancer associated fibroblast', 'neoplastic epithelial cell'
→ fix typos, remove non-existent values, or create objects via:
objects = bionty.CellType.from_values(['cancer associated fibroblast', 'neoplastic epithelial cell'], field='name').save()
xenium_curator.slots["tables:table:obs"].cat.add_new_from("celltype_major")
xenium_1_curated_af = xenium_curator.save_artifact(key="xenium1.zarr")
xenium_1_curated_af.describe()
Show code cell output
Artifact: xenium1.zarr (0000) ├── uid: XkumYeIswCvsgTzM0000 run: jWpvdsz (spatial3.ipynb) │ kind: dataset otype: SpatialData │ hash: C3YhhJSBuKdbJ9WLEnmuJw size: 34.9 MB │ branch: main space: all │ created_at: 2026-10-03 08:37:25 UTC created_by: testuser1 │ n_files: 105 schema: Spatialdata schema (flexible) ├── storage/path: /home/runner/work/lamin-usecases/lamin-usecases/docs/test-spatial/.lamindb/XkumYeIswCvsgTzM.zarr ├── Dataset features │ ├── attrs:sample (4) │ │ assay bionty.ExperimentalFactor 10x Xenium │ │ disease bionty.Disease ductal breast carcinoma in situ │ │ organism bionty.Organism human │ │ tissue bionty.Tissue breast │ ├── tables:table:obs (1) │ │ celltype_major bionty.CellType B cell, T cell, cancer associated fibr… │ └── tables:table:var.T (312 bion… │ ABCC11 num │ ACTA2 num │ ACTG2 num │ ADAM9 num │ ADGRE5 num │ ADH1B num │ ADIPOQ num │ AGR3 num │ AHSP num │ AIF1 num │ AKR1C1 num │ AKR1C3 num │ ALDH1A3 num │ ANGPT2 num │ ANKRD28 num │ ANKRD29 num │ ANKRD30A num │ APOBEC3A num │ APOBEC3B num │ APOC1 num │ AQP1 num │ AQP3 num │ AR num │ AVPR1A num │ BACE2 num │ BANK1 num │ BASP1 num │ BTNL9 num │ C1QA num │ C1QC num │ C2orf42 num │ C5orf46 num │ C6orf132 num │ CAV1 num │ CAVIN2 num │ CCDC6 num │ CCDC80 num │ CCL20 num │ CCL5 num │ CCL8 num │ CCND1 num │ CCPG1 num │ CCR7 num │ CD14 num │ CD163 num │ CD19 num │ CD1C num │ CD247 num │ CD27 num │ COXFA4L3 num └── Labels └── .organisms bionty.Organism human .tissues bionty.Tissue breast .cell_types bionty.CellType endothelial cell, myeloid cell, periva… .diseases bionty.Disease ductal breast carcinoma in situ .experimental_factors bionty.ExperimentalFactor 10x Xenium
Curate additional Xenium datasets¶
We can reuse the same curator for a second Xenium dataset:
xenium_aligned_2_sdata = db.Artifact.get(key="xenium_aligned_2_guide_min.zarr").load()
xenium_aligned_2_sdata.tables["table"].obs["celltype_major"] = (
xenium_aligned_2_sdata.tables["table"]
.obs["celltype_major"]
.cat.rename_categories(celltype_mapping)
)
Show code cell output
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/lamindb/core/storage/_zarr.py:119: UserWarning: SpatialData is not stored in the most current format. If you want to use Zarr v3, please write the store to a new location using `sdata.write()`.
scverse_obj = with_package("spatialdata", lambda mod: mod.read_zarr(store))
no parent found for <ome_zarr.reader.Label object at 0x7fba5820c2c0>: None
no parent found for <ome_zarr.reader.Label object at 0x7fba80c1f410>: None
→ Artifact xenium_aligned_2_guide_min.zarr: 1 transferred, 0 already on target
xenium_2_curated_af = ln.Artifact.from_spatialdata(
xenium_aligned_2_sdata, key="xenium2.zarr", schema=spatial_schema
).save()
Show code cell output
→ loading artifact into memory for validation
→ returning schema with same hash: Schema(uid='jWctqZOGVcTX6Q5w', is_type=False, name=None, description=None, n_members=4, coerce=True, flexible=False, itype='Feature', otype=None, suffix=None, hash='KQHDvynKIj-OlU2h3RUYrg', minimal_set=True, ordered_set=False, maximal_set=False, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, run_id=3, type_id=None, created_at=2026-10-03 08:37:25 UTC, is_locked=False)
→ returning schema with same hash: Schema(uid='71XtMhRTKEWJV9JK', is_type=False, name=None, description=None, n_members=1, coerce=True, flexible=False, itype='Feature', otype=None, suffix=None, hash='AdomA8jVxahbqeyFtGhSYw', minimal_set=True, ordered_set=False, maximal_set=False, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, run_id=3, type_id=None, created_at=2026-10-03 08:37:25 UTC, is_locked=False)
→ returning schema with same hash: Schema(uid='GhTeJl7B2uwsFAhy', is_type=False, name=None, description=None, n_members=312, coerce=None, flexible=False, itype='bionty.Gene.ensembl_gene_id', otype=None, suffix=None, hash='NJtMGWnH5ReYoEIQWHxA_A', minimal_set=True, ordered_set=False, maximal_set=False, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, run_id=3, type_id=None, created_at=2026-10-03 08:37:25 UTC, is_locked=False)
xenium_2_curated_af.describe()
Show code cell output
Artifact: xenium2.zarr (0000) ├── uid: HbBPuPpkLAcBj0rS0000 run: jWpvdsz (spatial3.ipynb) │ kind: dataset otype: SpatialData │ hash: 61vKPx35aFHukn6vLe6f2A size: 36.9 MB │ branch: main space: all │ created_at: 2026-10-03 08:37:31 UTC created_by: testuser1 │ n_files: 130 schema: Spatialdata schema (flexible) ├── storage/path: /home/runner/work/lamin-usecases/lamin-usecases/docs/test-spatial/.lamindb/HbBPuPpkLAcBj0rS.zarr ├── Dataset features │ ├── attrs:sample (4) │ │ assay bionty.ExperimentalFactor 10x Xenium │ │ disease bionty.Disease ductal breast carcinoma in situ │ │ organism bionty.Organism human │ │ tissue bionty.Tissue breast │ ├── tables:table:obs (1) │ │ celltype_major bionty.CellType B cell, T cell, cancer associated fibr… │ └── tables:table:var.T (312 bion… │ ABCC11 num │ ACTA2 num │ ACTG2 num │ ADAM9 num │ ADGRE5 num │ ADH1B num │ ADIPOQ num │ AGR3 num │ AHSP num │ AIF1 num │ AKR1C1 num │ AKR1C3 num │ ALDH1A3 num │ ANGPT2 num │ ANKRD28 num │ ANKRD29 num │ ANKRD30A num │ APOBEC3A num │ APOBEC3B num │ APOC1 num │ AQP1 num │ AQP3 num │ AR num │ AVPR1A num │ BACE2 num │ BANK1 num │ BASP1 num │ BTNL9 num │ C1QA num │ C1QC num │ C2orf42 num │ C5orf46 num │ C6orf132 num │ CAV1 num │ CAVIN2 num │ CCDC6 num │ CCDC80 num │ CCL20 num │ CCL5 num │ CCL8 num │ CCND1 num │ CCPG1 num │ CCR7 num │ CD14 num │ CD163 num │ CD19 num │ CD1C num │ CD247 num │ CD27 num │ COXFA4L3 num └── Labels └── .organisms bionty.Organism human .tissues bionty.Tissue breast .cell_types bionty.CellType endothelial cell, myeloid cell, periva… .diseases bionty.Disease ductal breast carcinoma in situ .experimental_factors bionty.ExperimentalFactor 10x Xenium
Curate Visium datasets¶
Analogously, we can define a Schema and Curator for Visium datasets:
visium_aligned_sdata = db.Artifact.get(key="visium_aligned_guide_min.zarr").load()
visium_aligned_sdata
Show code cell output
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/lamindb/core/storage/_zarr.py:119: UserWarning: SpatialData is not stored in the most current format. If you want to use Zarr v3, please write the store to a new location using `sdata.write()`.
scverse_obj = with_package("spatialdata", lambda mod: mod.read_zarr(store))
→ Artifact visium_aligned_guide_min.zarr: 1 transferred, 0 already on target
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/zarr/core/group.py:3413: ZarrUserWarning: Object at zmetadata is not recognized as a component of a Zarr hierarchy.
warnings.warn(
SpatialData object, with associated Zarr store: /home/runner/.cache/lamindb/lamindata/visium_aligned_guide_min.zarr
├── Images
│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_full_image': DataTree[cyx] (3, 1213, 952), (3, 607, 476), (3, 303, 238), (3, 152, 119), (3, 76, 60)
│ ├── 'CytAssist_FFPE_Human_Breast_Cancer_hires_image': DataArray[cyx] (3, 113, 88)
│ └── 'CytAssist_FFPE_Human_Breast_Cancer_lowres_image': DataArray[cyx] (3, 34, 27)
├── Shapes
│ └── 'CytAssist_FFPE_Human_Breast_Cancer': GeoDataFrame shape: (37, 2) (2D shapes)
└── Tables
└── 'table': AnnData (37, 18085)
with coordinate systems:
▸ 'aligned', with elements:
CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)
▸ 'downscaled_hires', with elements:
CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)
▸ 'downscaled_lowres', with elements:
CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)
▸ 'global', with elements:
CytAssist_FFPE_Human_Breast_Cancer_full_image (Images), CytAssist_FFPE_Human_Breast_Cancer_hires_image (Images), CytAssist_FFPE_Human_Breast_Cancer_lowres_image (Images), CytAssist_FFPE_Human_Breast_Cancer (Shapes)
visium_curated_af = ln.Artifact.from_spatialdata(
visium_aligned_sdata, key="visium.zarr", schema=spatial_schema
).save()
Show code cell output
→ loading artifact into memory for validation
! no values were validated for columns!
→ starting creation of 17759 Gene records in batches of 10000
→ returning schema with same hash: Schema(uid='jWctqZOGVcTX6Q5w', is_type=False, name=None, description=None, n_members=4, coerce=True, flexible=False, itype='Feature', otype=None, suffix=None, hash='KQHDvynKIj-OlU2h3RUYrg', minimal_set=True, ordered_set=False, maximal_set=False, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, run_id=3, type_id=None, created_at=2026-10-03 08:37:25 UTC, is_locked=False)
→ returning schema with same hash: Schema(uid='afZqSW2Fim51AUdP', is_type=False, name='Flexible metadata', description=None, n_members=None, coerce=True, flexible=True, itype='Feature', otype=None, suffix=None, hash='jKTX5yzmVwIdJdHH2ZfMAA', minimal_set=True, ordered_set=False, maximal_set=False, branch_id=1, created_on_id=1, space_id=1, created_by_id=1, run_id=3, type_id=None, created_at=2026-10-03 08:37:07 UTC, is_locked=False)
→ not annotating with 18065 features for slot tables:table:var.T as it exceeds 1000 (ln.settings.annotation.n_max_records)
visium_curated_af.describe()
Show code cell output
Artifact: visium.zarr (0000) ├── uid: 8WISIvM5b7EaXAw20000 run: jWpvdsz (spatial3.ipynb) │ kind: dataset otype: SpatialData │ hash: rxv3I6NY89MKOFO01OdHbQ size: 4.5 MB │ branch: main space: all │ created_at: 2026-10-03 08:37:53 UTC created_by: testuser1 │ n_files: 91 schema: Spatialdata schema (flexible) ├── storage/path: /home/runner/work/lamin-usecases/lamin-usecases/docs/test-spatial/.lamindb/8WISIvM5b7EaXAw2.zarr ├── Dataset features │ ├── attrs:sample (4) │ │ assay bionty.ExperimentalFactor Visium Spatial Gene Expression │ │ disease bionty.Disease ductal breast carcinoma in situ │ │ organism bionty.Organism human │ │ tissue bionty.Tissue breast │ ├── tables:table:obs (None) │ └── tables:table:var.T (18065 bi… └── Labels └── .organisms bionty.Organism human .tissues bionty.Tissue breast .diseases bionty.Disease ductal breast carcinoma in situ .experimental_factors bionty.ExperimentalFactor Visium Spatial Gene Expression
Overview of the curated datasets¶
visium_curated_af.view_lineage()
Show code cell output
ln.Artifact.to_dataframe(features=["assay", "organism", "tissue", "celltype_major"])
Show code cell output
| uid | key | organism | assay | tissue | celltype_major | |
|---|---|---|---|---|---|---|
| id | ||||||
| 10 | 8WISIvM5b7EaXAw20000 | visium.zarr | human | Visium Spatial Gene Expression | breast | NaN |
| 9 | bjH534dxVi1drmLZ0001 | visium_aligned_guide_min.zarr | NaN | NaN | NaN | NaN |
| 8 | HbBPuPpkLAcBj0rS0000 | xenium2.zarr | human | 10x Xenium | breast | {T cell, endothelial cell, plasmablast, cancer... |
| 7 | KFhRNPqcdoxBCNZt0001 | xenium_aligned_2_guide_min.zarr | NaN | NaN | NaN | NaN |
| 6 | XkumYeIswCvsgTzM0000 | xenium1.zarr | human | 10x Xenium | breast | {T cell, endothelial cell, plasmablast, cancer... |
| 5 | kVMuYil81BHTwQ9G0001 | xenium_aligned_1_guide_min.zarr | NaN | NaN | NaN | NaN |
| 4 | ysggpUJW75NJevhA0000 | example_blobs.zarr | NaN | NaN | NaN | NaN |
| 1 | 8sPWscz3SICG1D8t0001 | xenium/2.0.0/Xenium_V1_humanLung_Cancer_FFPE_o... | NaN | NaN | NaN | NaN |
ln.finish()
Show code cell output
→ finished Run('jWpvdszhKhwUCWWI') after 54s at 2026-10-03 08:37:54 UTC