Tutorial: Features & labels¶
In Tutorial: Artifacts, we learned about provenance and data access. Here, we walk through validating & annotating datasets with features & labels to improve:
Findability: Which collections measured expression of cell marker
CD14
? Which characterized cell lineK562
? Which collections have a test & train split? Etc.Usability: Are there typos in feature names? Are there typos in sampled labels? Are types and units of features consistent? Etc.
Hint
This is a low-level tutorial aimed at a basic understanding of registering features and labels for annotation & validation.
If you’re just looking to readily validate and annotate a dataset with features and labels, see this guide: Curate datasets.
import lamindb as ln
import pandas as pd
import pytest
ln.settings.verbosity = "hint"
Show code cell output
→ connected lamindb: anonymous/lamin-tutorial
TLDR¶
Annotate by labels¶
# create a label
study0 = ln.ULabel(name="Study 0: initial plant gathering", description="My initial study").save()
# query an artifact from the previous tutorial
artifact = ln.Artifact.get(key="iris_studies/study0_raw_images")
# label the artifact
artifact.labels.add(study0)
# look at artifact metadata
artifact.describe()
Show code cell output
Artifact(uid='rPUXg3LRErBTIXZR0000', is_latest=True, key='iris_studies/study0_raw_images', suffix='', size=658465, hash='IVKGMfNwi8zKvnpaD_gG7w', n_objects=51, _hash_type='md5-d', visibility=1, _key_is_virtual=False, created_at=2024-10-22 09:22:17 UTC)
Provenance
.storage = 's3://lamindata'
.transform = 'Tutorial: Artifacts'
.run = 2024-10-22 09:22:12 UTC
.created_by = 'anonymous'
Labels
.ulabels = 'Study 0: initial plant gathering'
Annotate by features¶
Features are buckets for labels, numbers and other data types.
Often, data that you want to ingest comes with metadata.
Here, three metadata features species
, scientist
, instrument
were collected.
df = pd.read_csv(artifact.path / "meta.csv", index_col=0)
df.head()
species | file_name | scientist | instrument | |
---|---|---|---|---|
0 | setosa | iris-0797945218a97d6e5251b4758a2ba1b418cbd52ce... | Barbara McClintock | Leica IIIc Camera |
1 | versicolor | iris-0f133861ea3fe1b68f9f1b59ebd9116ff963ee710... | Edgar Anderson | Leica IIIc Camera |
2 | versicolor | iris-9ffe51c2abd973d25a299647fa9ccaf6aa9c8eecf... | Edgar Anderson | Leica IIIc Camera |
3 | setosa | iris-83f433381b755101b9fc9fbc9743e35fbb8a1a109... | Edgar Anderson | Leica IIIc Camera |
4 | virginica | iris-bdae8314e4385d8e2322abd8e63a82758a9063c77... | Edgar Anderson | Leica IIIc Camera |
There are only a few values for features species
, scientist
& instrument
, and we’d like to label the artifact with these values:
df.nunique()
species 3
file_name 50
scientist 2
instrument 1
dtype: int64
Let’s annotate the artifact with features & values and add a temperature
measurement that Barbara & Edgar had forgotten in their csv:
with pytest.raises(ln.core.exceptions.ValidationError) as error:
artifact.features.add_values({"species": df.species.unique(), "scientist": df.scientist.unique(), "instrument": df.instrument.unique(), "temperature": 27.6, "study": "Study 0: initial plant gathering"})
print(error.exconly())
Show code cell output
lamindb.core.exceptions.ValidationError: These keys could not be validated: ['species', 'scientist', 'instrument', 'temperature', 'study']
Here is how to create a feature:
ln.Feature(name='species', dtype='cat').save()
ln.Feature(name='scientist', dtype='cat').save()
ln.Feature(name='instrument', dtype='cat').save()
ln.Feature(name='temperature', dtype='float').save()
ln.Feature(name='study', dtype='cat[ULabel]').save()
As we saw, nothing was validated and hence, we got an error that tells us to register features & labels:
ln.Feature(name='species', dtype='cat[ULabel]').save()
ln.Feature(name='scientist', dtype='cat[ULabel]').save()
ln.Feature(name='instrument', dtype='cat[ULabel]').save()
ln.Feature(name='study', dtype='cat[ULabel]').save()
ln.Feature(name='temperature', dtype='float').save()
species = ln.ULabel.from_values(df['species'].unique(), create=True)
ln.save(species)
authors = ln.ULabel.from_values(df['scientist'].unique(), create=True)
ln.save(authors)
instruments = ln.ULabel.from_values(df['instrument'].unique(), create=True)
ln.save(instruments)
Now everything works:
artifact.features.add_values({"species": df.species.unique(), "scientist": df.scientist.unique(), "instrument": df.instrument.unique(), "temperature": 27.6, "study": "Study 0: initial plant gathering"})
artifact.describe()
Show code cell output
Artifact(uid='rPUXg3LRErBTIXZR0000', is_latest=True, key='iris_studies/study0_raw_images', suffix='', size=658465, hash='IVKGMfNwi8zKvnpaD_gG7w', n_objects=51, _hash_type='md5-d', visibility=1, _key_is_virtual=False, created_at=2024-10-22 09:22:17 UTC)
Provenance
.storage = 's3://lamindata'
.transform = 'Tutorial: Artifacts'
.run = 2024-10-22 09:22:12 UTC
.created_by = 'anonymous'
Labels
.ulabels = 'Study 0: initial plant gathering', 'setosa', 'versicolor', 'virginica', 'Barbara McClintock', 'Edgar Anderson', 'Leica IIIc Camera'
Features
'instrument' = 'Leica IIIc Camera'
'scientist' = 'Barbara McClintock', 'Edgar Anderson'
'species' = 'setosa', 'versicolor', 'virginica'
'study' = 'Study 0: initial plant gathering'
'temperature' = 27.6
Because we also re-labeled with the study label Study 0: initial plant gathering'
, we see that it appears under the study
feature.
Retrieve features¶
artifact.features.get_values()
Show code cell output
{'species': ['setosa', 'versicolor', 'virginica'],
'scientist': ['Barbara McClintock', 'Edgar Anderson'],
'instrument': 'Leica IIIc Camera',
'study': 'Study 0: initial plant gathering',
'temperature': 27.6}
Query by features¶
artifact = ln.Artifact.features.get(temperature=27.6)
artifact
Artifact(uid='rPUXg3LRErBTIXZR0000', is_latest=True, key='iris_studies/study0_raw_images', suffix='', size=658465, hash='IVKGMfNwi8zKvnpaD_gG7w', n_objects=51, _hash_type='md5-d', visibility=1, _key_is_virtual=False, storage_id=2, transform_id=1, run_id=1, created_by_id=1, created_at=2024-10-22 09:22:17 UTC)
Register metadata¶
Features and labels are the primary ways of registering domain-knowledge related metadata in LaminDB.
Features represent measurement dimensions (e.g. "species"
) and labels represent measured values (e.g. "iris setosa"
, "iris versicolor"
, "iris virginica"
).
In statistics, you’d say a feature is a categorical or numerical variable while a label is a simple category. Categorical variables draw their values from a set of categories.
Register labels¶
We study 3 species of the Iris plant: setosa
, versicolor
& virginica
. Let’s create 3 labels with ULabel
.
ULabel
enables you to manage an in-house ontology to manage all kinds of generic labels.
What are alternatives to ULabel?
In a complex project, you’ll likely want dedicated typed registries for selected label types, e.g., Gene
, Tissue
, etc. See: Manage biological registries.
ULabel
, however, will get you quite far and scale to ~1M labels.
Anticipating that we’ll have many different labels when working with more data, we’d like to express that all 3 labels are species labels:
is_species = ln.ULabel(name="is_species").save()
is_species.children.set(species)
is_species.view_parents(with_children=True)
Show code cell output
Query artifacts by labels¶
Using the new annotations, you can now query image artifacts by species & study labels:
ln.ULabel.df()
uid | name | description | reference | reference_type | run_id | created_at | created_by_id | |
---|---|---|---|---|---|---|---|---|
id | ||||||||
8 | 2dWlxaDr | is_species | None | None | None | None | 2024-10-22 09:22:29.356090+00:00 | 1 |
7 | idmniRZt | Leica IIIc Camera | None | None | None | None | 2024-10-22 09:22:29.212544+00:00 | 1 |
6 | n8W9uPfH | Edgar Anderson | None | None | None | None | 2024-10-22 09:22:29.207976+00:00 | 1 |
5 | uyDUMilH | Barbara McClintock | None | None | None | None | 2024-10-22 09:22:29.207882+00:00 | 1 |
4 | o2UaTLuJ | virginica | None | None | None | None | 2024-10-22 09:22:29.200461+00:00 | 1 |
3 | YJ9Cl9um | versicolor | None | None | None | None | 2024-10-22 09:22:29.200390+00:00 | 1 |
2 | OopAYMpu | setosa | None | None | None | None | 2024-10-22 09:22:29.200297+00:00 | 1 |
1 | 13GuIO1j | Study 0: initial plant gathering | My initial study | None | None | None | 2024-10-22 09:22:28.358588+00:00 | 1 |
ulabels = ln.ULabel.lookup()
ln.Artifact.get(ulabels=ulabels.study_0_initial_plant_gathering)
Show code cell output
Artifact(uid='rPUXg3LRErBTIXZR0000', is_latest=True, key='iris_studies/study0_raw_images', suffix='', size=658465, hash='IVKGMfNwi8zKvnpaD_gG7w', n_objects=51, _hash_type='md5-d', visibility=1, _key_is_virtual=False, storage_id=2, transform_id=1, run_id=1, created_by_id=1, created_at=2024-10-22 09:22:17 UTC)
Run an ML model¶
Let’s now run a mock ML model that transforms the images into 4 high-level features.
def run_ml_model() -> pd.DataFrame:
image_file_dir = artifact.cache()
output_data = ln.core.datasets.df_iris_in_meter_study1()
return output_data
transform = ln.Transform(name="Petal & sepal regressor", type="pipeline")
ln.context.track(transform=transform)
df = run_ml_model()
Show code cell output
• tracked pip freeze > /home/runner/.cache/lamindb/run_env_pip_atvLo4800drM4Idx3QB6.txt
→ created Transform('zPKcgBaX'), started new Run('atvLo480') at 2024-10-22 09:22:29 UTC
• adding artifact ids [1] as inputs for run 2, adding parent transform 1
The output is a dataframe:
df.head()
Show code cell output
sepal_length | sepal_width | petal_length | petal_width | iris_organism_name | |
---|---|---|---|---|---|
0 | 0.051 | 0.035 | 0.014 | 0.002 | setosa |
1 | 0.049 | 0.030 | 0.014 | 0.002 | setosa |
2 | 0.047 | 0.032 | 0.013 | 0.002 | setosa |
3 | 0.046 | 0.031 | 0.015 | 0.002 | setosa |
4 | 0.050 | 0.036 | 0.014 | 0.002 | setosa |
And this is the pipeline that produced the dataframe:
ln.context.transform.view_lineage()
Show code cell output
Register the output data¶
Let’s first register the features of the transformed data:
new_features = ln.Feature.from_df(df)
ln.save(new_features)
How to track units of features?
Use the unit
field of Feature
. In the above example, you’d do:
for feature in features:
if feature.type == "number":
feature.unit = "m" # SI unit for meters
feature.save()
We can now validate & register the dataframe in one line:
artifact = ln.Artifact.from_df(
df,
description="Iris study 1 - after measuring sepal & petal metrics",
)
artifact.save()
Show code cell output
• path content will be copied to default storage upon `save()` with key `None` ('.lamindb/n2pAGCLBEZe5cxrs0000.parquet')
✓ storing artifact 'n2pAGCLBEZe5cxrs0000' at '/home/runner/work/lamindb/lamindb/docs/lamin-tutorial/.lamindb/n2pAGCLBEZe5cxrs0000.parquet'
Artifact(uid='n2pAGCLBEZe5cxrs0000', is_latest=True, description='Iris study 1 - after measuring sepal & petal metrics', suffix='.parquet', type='dataset', size=5347, hash='hc1_5GpqDON87kh_GvkIqg', _hash_type='md5', _accessor='DataFrame', visibility=1, _key_is_virtual=True, storage_id=1, transform_id=2, run_id=2, created_by_id=1, created_at=2024-10-22 09:22:31 UTC)
There is one categorical feature, let’s add the species labels:
features = ln.Feature.lookup()
species_labels = ln.ULabel.filter(parents__name="is_species").all()
artifact.labels.add(species_labels, feature=features.species)
species_labels
<QuerySet [ULabel(uid='OopAYMpu', name='setosa', created_by_id=1, created_at=2024-10-22 09:22:29 UTC), ULabel(uid='YJ9Cl9um', name='versicolor', created_by_id=1, created_at=2024-10-22 09:22:29 UTC), ULabel(uid='o2UaTLuJ', name='virginica', created_by_id=1, created_at=2024-10-22 09:22:29 UTC)]>
Let’s now add study labels:
artifact.labels.add(ulabels.study_0_initial_plant_gathering, feature=features.study)
This is the context for our artifact:
artifact.describe()
artifact.view_lineage()
Show code cell output
Artifact(uid='n2pAGCLBEZe5cxrs0000', is_latest=True, description='Iris study 1 - after measuring sepal & petal metrics', suffix='.parquet', type='dataset', size=5347, hash='hc1_5GpqDON87kh_GvkIqg', _hash_type='md5', _accessor='DataFrame', visibility=1, _key_is_virtual=True, created_at=2024-10-22 09:22:31 UTC)
Provenance
.storage = '/home/runner/work/lamindb/lamindb/docs/lamin-tutorial'
.transform = 'Petal & sepal regressor'
.run = 2024-10-22 09:22:29 UTC
.created_by = 'anonymous'
Labels
.ulabels = 'Study 0: initial plant gathering', 'setosa', 'versicolor', 'virginica'
Features
'species' = 'setosa', 'versicolor', 'virginica'
'study' = 'Study 0: initial plant gathering'
See the database content:
ln.view(registries=["Feature", "ULabel"])
Show code cell output
Feature
uid | name | dtype | unit | description | synonyms | run_id | created_at | created_by_id | |
---|---|---|---|---|---|---|---|---|---|
id | |||||||||
10 | f6fgflXi4bCX | iris_organism_name | cat | None | None | None | 2.0 | 2024-10-22 09:22:31.769212+00:00 | 1 |
9 | PZuAT2xRPJFS | petal_width | float | None | None | None | 2.0 | 2024-10-22 09:22:31.769136+00:00 | 1 |
8 | RR86aCyJ5WG5 | petal_length | float | None | None | None | 2.0 | 2024-10-22 09:22:31.769058+00:00 | 1 |
7 | guELj1Oujo40 | sepal_width | float | None | None | None | 2.0 | 2024-10-22 09:22:31.768978+00:00 | 1 |
6 | UrKvfVwJfB5X | sepal_length | float | None | None | None | 2.0 | 2024-10-22 09:22:31.768867+00:00 | 1 |
5 | ROvDYAagCiRt | temperature | float | None | None | None | NaN | 2024-10-22 09:22:29.191663+00:00 | 1 |
4 | MtS7nWqfcC9q | study | cat[ULabel] | None | None | None | NaN | 2024-10-22 09:22:29.187676+00:00 | 1 |
ULabel
uid | name | description | reference | reference_type | run_id | created_at | created_by_id | |
---|---|---|---|---|---|---|---|---|
id | ||||||||
8 | 2dWlxaDr | is_species | None | None | None | None | 2024-10-22 09:22:29.356090+00:00 | 1 |
7 | idmniRZt | Leica IIIc Camera | None | None | None | None | 2024-10-22 09:22:29.212544+00:00 | 1 |
6 | n8W9uPfH | Edgar Anderson | None | None | None | None | 2024-10-22 09:22:29.207976+00:00 | 1 |
5 | uyDUMilH | Barbara McClintock | None | None | None | None | 2024-10-22 09:22:29.207882+00:00 | 1 |
4 | o2UaTLuJ | virginica | None | None | None | None | 2024-10-22 09:22:29.200461+00:00 | 1 |
3 | YJ9Cl9um | versicolor | None | None | None | None | 2024-10-22 09:22:29.200390+00:00 | 1 |
2 | OopAYMpu | setosa | None | None | None | None | 2024-10-22 09:22:29.200297+00:00 | 1 |
This is it! 😅
If you’re interested, please check out guides & use cases or make an issue on GitHub to discuss.
Appendix¶
Manage metadata¶
Avoid duplicates¶
Let’s create a label "project1"
:
ln.ULabel(name="project1").save()
Show code cell output
ULabel(uid='xnG6Akuh', name='project1', created_by_id=1, run_id=2, created_at=2024-10-22 09:22:32 UTC)
We already created a project1
label before, let’s see what happens if we try to create it again:
label = ln.ULabel(name="project1")
label.save()
Show code cell output
→ returning existing ULabel record with same name: 'project1'
ULabel(uid='xnG6Akuh', name='project1', created_by_id=1, run_id=2, created_at=2024-10-22 09:22:32 UTC)
Instead of creating a new record, LaminDB loads and returns the existing record from the database.
If there is no exact match, LaminDB will warn you upon creating a record about potential duplicates.
Say, we spell “project 1” with a white space:
ln.ULabel(name="project 1")
Show code cell output
! record with similar name exists! did you mean to load it?
uid | name | description | reference | reference_type | run_id | created_at | created_by_id | |
---|---|---|---|---|---|---|---|---|
id | ||||||||
9 | xnG6Akuh | project1 | None | None | None | 2 | 2024-10-22 09:22:32.011481+00:00 | 1 |
ULabel(uid='Th4BqRjy', name='project 1', created_by_id=1, run_id=2)
To avoid inserting duplicates when creating new records, a search compares whether a similar record already exists.
You can switch it off for performance gains via search_names
.
Update & delete records¶
label = ln.ULabel.filter(name="project1").first()
label
Show code cell output
ULabel(uid='xnG6Akuh', name='project1', created_by_id=1, run_id=2, created_at=2024-10-22 09:22:32 UTC)
label.name = "project1a"
label.save()
label
Show code cell output
ULabel(uid='xnG6Akuh', name='project1a', created_by_id=1, run_id=2, created_at=2024-10-22 09:22:32 UTC)
label.delete()
Manage storage¶
Change default storage¶
The default storage location is:
ln.settings.storage
Show code cell output
StorageSettings(root='/home/runner/work/lamindb/lamindb/docs/lamin-tutorial', uid='rWEVGeFlAsUE')
You can change it by setting ln.settings.storage = "s3://my-bucket"
.
See all storage locations¶
ln.Storage.df()
Show code cell output
uid | root | description | type | region | instance_uid | run_id | created_at | created_by_id | |
---|---|---|---|---|---|---|---|---|---|
id | |||||||||
2 | SgmbmdCPto0R | s3://lamindata | None | s3 | us-east-1 | None | None | 2024-10-22 09:22:17.180281+00:00 | 1 |
1 | rWEVGeFlAsUE | /home/runner/work/lamindb/lamindb/docs/lamin-t... | None | local | None | 5WuFt3cW4zRx | None | 2024-10-22 09:22:07.648178+00:00 | 1 |
Show code cell content
# clean up what we wrote in this notebook
!rm -r lamin-tutorial
!lamin delete --force lamin-tutorial
! calling anonymously, will miss private instances
• deleting instance anonymous/lamin-tutorial