Skill Tracer
Skills › Scientific Agent Skills › lamindb › annotation-validation.md

annotation-validation.md

158 lines · skills/lamindb/references/annotation-validation.md at 5f2bc03

Open on GitHub
1# Annotation and validation
2
3Targets LaminDB 2.10.0. Curation checks schema constraints and adds queryable
4annotations. It does not establish correct experimental interpretation.
5

Annotation and validation

Targets LaminDB 2.10.0. Curation checks schema constraints and adds queryable annotations. It does not establish correct experimental interpretation.

Raw lines 1 to 5
6## Choose schema semantics explicitly
7
8```python
9import lamindb as ln
11count = ln.Feature(name="gene_count", dtype=int).save()
12condition = ln.Feature(name="condition", dtype=str).save()
14# Require these columns; other columns can remain unvalidated.
15minimal = ln.Schema(name="qc_minimal", features=[count, condition]).save()
17# Validate/annotate additional columns against the Feature registry too.
18flexible = ln.Schema(
19 name="qc_flexible", features=[count, condition], flexible=True
20).save()
22# Require precisely this feature set (order is independent unless requested).
23strict = ln.Schema(
24 name="qc_strict", features=[count, condition], maximal_set=True
25).save()
26```
28`minimal_set=True` requires listed features by default. `maximal_set=True`
29forbids extra features; `ordered_set=True` additionally requires their order.
30`flexible` determines whether other encountered features are validated and
31annotated. In particular, **`flexible=False` does not forbid extra columns**.
32A feature-only registry schema `ln.Schema(itype=ln.Feature)` defaults to flexible;
33unregistered column names can then require curation rather than being ignored.
35Use `coerce=True` on `Feature` or `Schema` only after deciding a conversion is
36scientifically acceptable. `coerce_dtype` is not a current Feature argument.
37`nullable` and default values must reflect the assay's missing-data semantics.

Choose schema semantics explicitly

import lamindb as ln

count = ln.Feature(name="gene_count", dtype=int).save()
condition = ln.Feature(name="condition", dtype=str).save()

# Require these columns; other columns can remain unvalidated.
minimal = ln.Schema(name="qc_minimal", features=[count, condition]).save()

# Validate/annotate additional columns against the Feature registry too.
flexible = ln.Schema(
    name="qc_flexible", features=[count, condition], flexible=True
).save()

# Require precisely this feature set (order is independent unless requested).
strict = ln.Schema(
    name="qc_strict", features=[count, condition], maximal_set=True
).save()

minimal_set=True requires listed features by default. maximal_set=True forbids extra features; ordered_set=True additionally requires their order. flexible determines whether other encountered features are validated and annotated. In particular, **flexible=False does not forbid extra columns**. A feature-only registry schema ln.Schema(itype=ln.Feature) defaults to flexible; unregistered column names can then require curation rather than being ignored.

Use coerce=True on Feature or Schema only after deciding a conversion is scientifically acceptable. coerce_dtype is not a current Feature argument. nullable and default values must reflect the assay's missing-data semantics.

Raw lines 6 to 38
39## Validate, repair, then save
41```python
42import pandas as pd
44frame = pd.DataFrame({"gene_count": [120, 130], "condition": ["control", "treated"]})
45curator = ln.curators.DataFrameCurator(frame, strict)
46curator.validate() # None means normal return; invalid data raises ValidationError.
47artifact = curator.save_artifact(key="experiments/qc.parquet")
48assert artifact.schema == strict
49pd.testing.assert_frame_equal(artifact.load(), frame)
50```
52For failures, catch `ln.errors.ValidationError` to inspect the message and stop
53registration. `curator.cat.non_validated` describes unresolved categorical
54values after their validation stage; it is not a replacement for dtype/shape
55errors. Do not test `if curator.validate():` or blindly create new valid labels
56to silence an error. After changing the underlying DataFrame, recreate the
57curator and validate the complete corrected data again.
59A shortcut for already clean data is:
61```python
62artifact = ln.Artifact.from_dataframe(
63 frame, key="experiments/qc-direct.parquet", schema=strict
64).save()
65```

Validate, repair, then save

import pandas as pd

frame = pd.DataFrame({"gene_count": [120, 130], "condition": ["control", "treated"]})
curator = ln.curators.DataFrameCurator(frame, strict)
curator.validate()  # None means normal return; invalid data raises ValidationError.
artifact = curator.save_artifact(key="experiments/qc.parquet")
assert artifact.schema == strict
pd.testing.assert_frame_equal(artifact.load(), frame)

For failures, catch ln.errors.ValidationError to inspect the message and stop registration. curator.cat.non_validated describes unresolved categorical values after their validation stage; it is not a replacement for dtype/shape errors. Do not test if curator.validate(): or blindly create new valid labels to silence an error. After changing the underlying DataFrame, recreate the curator and validate the complete corrected data again.

A shortcut for already clean data is:

artifact = ln.Artifact.from_dataframe(
    frame, key="experiments/qc-direct.parquet", schema=strict
).save()
Raw lines 39 to 66
67## Ontology-backed categorical columns
69The registry association belongs in the **feature dtype**:
71```python
72import bionty as bt
74cell_type = ln.Feature(name="cell_type", dtype=bt.CellType).save()
75cell_schema = ln.Schema(name="cell_metadata", features=[cell_type], coerce=True).save()
76# df must contain reviewed Cell Ontology names/IDs matching the chosen dtype.
77# curator = ln.curators.DataFrameCurator(df, cell_schema)
78# curator.validate()
79# curator.cat.standardize("cell_type")
80# curator.validate()
81```
83A plain `dtype=str` does not validate against Cell Ontology. Use
84`dtype=bt.CellType.ontology_id` when input labels are ontology IDs. Review the
85source and mappings using [ontology management](ontologies.md).
86`curator.cat.lookup(public=True)` can aid discovery. `add_ontology()` and
87`inspect_standardize()` are not methods in this release. Public source lookup
88can perform downloads and is not an offline guarantee.
90`cat.standardize("cell_type")` mutates the in-memory data by replacing known
91synonyms. `cat.add_new_from("cell_type")` writes registry records; use it only
92for explicitly accepted custom terms and preserve their provenance. A mismatch
93should not silently become a new biological category.

Ontology-backed categorical columns

The registry association belongs in the feature dtype:

import bionty as bt

cell_type = ln.Feature(name="cell_type", dtype=bt.CellType).save()
cell_schema = ln.Schema(name="cell_metadata", features=[cell_type], coerce=True).save()
# df must contain reviewed Cell Ontology names/IDs matching the chosen dtype.
# curator = ln.curators.DataFrameCurator(df, cell_schema)
# curator.validate()
# curator.cat.standardize("cell_type")
# curator.validate()

A plain dtype=str does not validate against Cell Ontology. Use dtype=bt.CellType.ontology_id when input labels are ontology IDs. Review the source and mappings using ontology management. curator.cat.lookup(public=True) can aid discovery. add_ontology() and inspect_standardize() are not methods in this release. Public source lookup can perform downloads and is not an offline guarantee.

cat.standardize("cell_type") mutates the in-memory data by replacing known synonyms. cat.add_new_from("cell_type") writes registry records; use it only for explicitly accepted custom terms and preserve their provenance. A mismatch should not silently become a new biological category.

Raw lines 67 to 94
95## AnnData slots
97Source-verified example for a user-supplied `adata`, after the selected gene and
98cell-type records are available in the instance:
100```python
101obs_schema = ln.Schema(name="obs_metadata", features=[cell_type], coerce=True).save()
102var_schema = ln.Schema(itype=bt.Gene.ensembl_gene_id, dtype=float).save()
103anndata_schema = ln.Schema(
104 name="scrna_schema",
105 otype="AnnData",
106 slots={"obs": obs_schema, "var.T": var_schema},
107).save()
108curator = ln.curators.AnnDataCurator(adata, anndata_schema)
109curator.validate()
110# If validation reported synonyms, review before standardizing:
111# curator.slots["obs"].cat.standardize("cell_type")
112# curator.validate()
113artifact = curator.save_artifact(key="scrna/validated.h5ad")
114```
116`var.T` treats the gene index as feature identifiers. Defining a single
117`Feature(name="ensembl_gene_id")` in a schema does not validate every gene ID.
118Set the correct organism/source for Gene operations and check uniqueness;
119gene symbols alone can be ambiguous. AnnData uses `curator.slots["obs"].cat`
120or `curator.slots["var.T"].cat`, not `curator.cat.standardize("obs", ...)`.
122The local regression suite exercises obs-slot curation, synonym replacement,
123H5AD save/load, and backed slicing with synthetic local Bionty records. It does
124not claim a downloaded Ensembl/Cell Ontology ingestion test.

AnnData slots

Source-verified example for a user-supplied adata, after the selected gene and cell-type records are available in the instance:

obs_schema = ln.Schema(name="obs_metadata", features=[cell_type], coerce=True).save()
var_schema = ln.Schema(itype=bt.Gene.ensembl_gene_id, dtype=float).save()
anndata_schema = ln.Schema(
    name="scrna_schema",
    otype="AnnData",
    slots={"obs": obs_schema, "var.T": var_schema},
).save()
curator = ln.curators.AnnDataCurator(adata, anndata_schema)
curator.validate()
# If validation reported synonyms, review before standardizing:
# curator.slots["obs"].cat.standardize("cell_type")
# curator.validate()
artifact = curator.save_artifact(key="scrna/validated.h5ad")

var.T treats the gene index as feature identifiers. Defining a single Feature(name="ensembl_gene_id") in a schema does not validate every gene ID. Set the correct organism/source for Gene operations and check uniqueness; gene symbols alone can be ambiguous. AnnData uses curator.slots["obs"].cat or curator.slots["var.T"].cat, not curator.cat.standardize("obs", ...).

The local regression suite exercises obs-slot curation, synonym replacement, H5AD save/load, and backed slicing with synthetic local Bionty records. It does not claim a downloaded Ensembl/Cell Ontology ingestion test.

Raw lines 95 to 125
126## Other composite formats
128These are source-verified slot contracts; optional backend execution is
129illustrative and needs a separately compatible environment:
131| Curator | Schema `otype` | Example slot names |
132| --- | --- | --- |
133| `MuDataCurator` | `MuData` | `obs`, `rna:obs`, `rna:var.T`, `uns:study_metadata` |
134| `SpatialDataCurator` | `SpatialData` | `tables:cell_metadata:obs`, `attrs:bio` |
135| `TiledbsomaExperimentCurator` | `tiledbsoma` | `obs`, `ms:RNA.T` |
137Each slot maps to a real saved Schema for that component. Pass a valid existing
138SOMA Experiment, not an empty collection masquerading as an expression dataset.
139The reviewed LaminDB dependency cap is AnnData <=0.13.2; do not assume arbitrary
140AnnData/TileDB-SOMA combinations can ingest each other.

Other composite formats

These are source-verified slot contracts; optional backend execution is illustrative and needs a separately compatible environment:

CuratorSchema otypeExample slot names
MuDataCuratorMuDataobs, rna:obs, rna:var.T, uns:study_metadata
SpatialDataCuratorSpatialDatatables:cell_metadata:obs, attrs:bio
TiledbsomaExperimentCuratortiledbsomaobs, ms:RNA.T

Each slot maps to a real saved Schema for that component. Pass a valid existing SOMA Experiment, not an empty collection masquerading as an expression dataset. The reviewed LaminDB dependency cap is AnnData <=0.13.2; do not assume arbitrary AnnData/TileDB-SOMA combinations can ingest each other.

Raw lines 126 to 141
142## Provenance and scientific checks
144Schemas are content-defined and can deduplicate equivalent definitions. They
145have no `version`/`version_tag` constructor field in 2.10.0. Create and save a new
146schema for a changed contract and retain the exact schema UID/hash with outputs.
147Do not mutate schema membership to retroactively imply old data were validated
148against new constraints. Query `Artifact.filter(schema=schema)`, not
149`Artifact.filter(is_valid=True)`.
151Before saving scientific results also check measurement units, matrix/layer
152meaning, finite values, allowed missingness, identifier collisions, and whether
153normalization or filtering changes biological interpretation. A universal
154expression-value ceiling is not meaningful across counts and normalized assays.
156Sources: [curation guide](https://docs.lamin.ai/curate),
157[Schema source](https://github.com/laminlabs/lamindb/blob/2.10.0/lamindb/models/schema.py),
158[curator source](https://github.com/laminlabs/lamindb/blob/2.10.0/lamindb/curators/core.py).

Provenance and scientific checks

Schemas are content-defined and can deduplicate equivalent definitions. They have no version/version_tag constructor field in 2.10.0. Create and save a new schema for a changed contract and retain the exact schema UID/hash with outputs. Do not mutate schema membership to retroactively imply old data were validated against new constraints. Query Artifact.filter(schema=schema), not Artifact.filter(is_valid=True).

Before saving scientific results also check measurement units, matrix/layer meaning, finite values, allowed missingness, identifier collisions, and whether normalization or filtering changes biological interpretation. A universal expression-value ceiling is not meaningful across counts and normalized assays.

Sources: curation guide, Schema source, curator source.

Raw lines 142 to 158