Skill Tracer
Skills › Scientific Agent Skills › cellxgene-census › core_workflow_patterns.md

core_workflow_patterns.md

304 lines · skills/cellxgene-census/references/core_workflow_patterns.md at 5f2bc03

Open on GitHub
1# Core Workflow Patterns
2
3Reviewed against Census 1.18.0, TileDB-SOMA 2.3.0, and the 2025-11-08 LTS.
4Snippets share an open `census` context unless shown otherwise. Large cohort,
5model-training, and downstream analysis blocks are illustrative: first inspect
6metadata and size, restrict datasets/genes, and supply the named callbacks/model.
7API and sparse-statistics contracts were exercised on a small local SOMA fixture;
8a 10-cell/3-gene slice and metadata were also read from the public LTS.
9
11The eight patterns in full, with code: opening the Census, exploring Census information,
12querying expression data at small to medium scale, large-scale out-of-core queries,
13machine learning with PyTorch, spatial Census data, Scanpy integration, and
14multi-dataset integration.

Core Workflow Patterns

Reviewed against Census 1.18.0, TileDB-SOMA 2.3.0, and the 2025-11-08 LTS. Snippets share an open census context unless shown otherwise. Large cohort, model-training, and downstream analysis blocks are illustrative: first inspect metadata and size, restrict datasets/genes, and supply the named callbacks/model. API and sparse-statistics contracts were exercised on a small local SOMA fixture; a 10-cell/3-gene slice and metadata were also read from the public LTS.

The eight patterns in full, with code: opening the Census, exploring Census information, querying expression data at small to medium scale, large-scale out-of-core queries, machine learning with PyTorch, spatial Census data, Scanpy integration, and multi-dataset integration.

Raw lines 1 to 15
16## Core Workflow Patterns

Core Workflow Patterns

Raw lines 16 to 17
18### 1. Opening the Census
20Always use the context manager to ensure proper resource cleanup:
22```python
23import cellxgene_census
25# Resolve a moving alias once, then record the concrete build
26build = cellxgene_census.get_census_version_description("stable")["release_build"]
27with cellxgene_census.open_soma(census_version=build) as census:
28 print(build, list(census["census_data"]))
30# Open the reviewed LTS version for reproducibility
31with cellxgene_census.open_soma(census_version="2025-11-08") as census:
32 print(list(census["census_data"]))
33```
35**Key points:**
36- Use context manager (`with` statement) for automatic cleanup
37- Specify `census_version` for reproducible analyses
38- `stable` opens the current LTS Census release; `latest` opens the newest weekly release retained for a shorter period

1. Opening the Census

Always use the context manager to ensure proper resource cleanup:

import cellxgene_census

# Resolve a moving alias once, then record the concrete build
build = cellxgene_census.get_census_version_description("stable")["release_build"]
with cellxgene_census.open_soma(census_version=build) as census:
    print(build, list(census["census_data"]))

# Open the reviewed LTS version for reproducibility
with cellxgene_census.open_soma(census_version="2025-11-08") as census:
    print(list(census["census_data"]))

Key points:

  • Use context manager (with statement) for automatic cleanup
  • Specify census_version for reproducible analyses
  • stable opens the current LTS Census release; latest opens the newest weekly release retained for a shorter period
Raw lines 18 to 39
40### 2. Exploring Census Information
42Before querying expression data, explore available datasets and metadata.
44**Access summary information:**
45```python
46# Get summary statistics as label/value rows
47summary = census["census_info"]["summary"].read().concat().to_pandas()
48summary_values = summary.set_index("label")["value"]
49print(f"Total cells: {int(summary_values['total_cell_count']):,}")
50print(f"Unique cells: {int(summary_values['unique_cell_count']):,}")
52# Get all datasets
53datasets = census["census_info"]["datasets"].read().concat().to_pandas()
55# Get precomputed counts by organism, cell type, tissue, disease, and assay
56summary_counts = census["census_info"]["summary_cell_counts"].read().concat().to_pandas()
57tissue_counts = summary_counts[summary_counts["category"].eq("tissue_general")]
58```
60**Query cell metadata to understand available data:**
61```python
62# Get unique cell types in a tissue
63cell_metadata = cellxgene_census.get_obs(
64 census,
65 "homo_sapiens",
66 value_filter="tissue_general == 'brain' and is_primary_data == True",
67 column_names=["cell_type"]
69unique_cell_types = cell_metadata["cell_type"].unique()
70print(f"Found {len(unique_cell_types)} cell types in brain")
72# Count cells by tissue
73tissue_metadata = cellxgene_census.get_obs(
74 census,
75 "homo_sapiens",
76 value_filter="is_primary_data == True",
77 column_names=["tissue_general"],
79tissue_counts = tissue_metadata["tissue_general"].value_counts()
80```
82**Important:** Always filter for `is_primary_data == True` to avoid counting duplicate cells unless specifically analyzing duplicates.

2. Exploring Census Information

Before querying expression data, explore available datasets and metadata.

Access summary information:

# Get summary statistics as label/value rows
summary = census["census_info"]["summary"].read().concat().to_pandas()
summary_values = summary.set_index("label")["value"]
print(f"Total cells: {int(summary_values['total_cell_count']):,}")
print(f"Unique cells: {int(summary_values['unique_cell_count']):,}")

# Get all datasets
datasets = census["census_info"]["datasets"].read().concat().to_pandas()

# Get precomputed counts by organism, cell type, tissue, disease, and assay
summary_counts = census["census_info"]["summary_cell_counts"].read().concat().to_pandas()
tissue_counts = summary_counts[summary_counts["category"].eq("tissue_general")]

Query cell metadata to understand available data:

# Get unique cell types in a tissue
cell_metadata = cellxgene_census.get_obs(
    census,
    "homo_sapiens",
    value_filter="tissue_general == 'brain' and is_primary_data == True",
    column_names=["cell_type"]
)
unique_cell_types = cell_metadata["cell_type"].unique()
print(f"Found {len(unique_cell_types)} cell types in brain")

# Count cells by tissue
tissue_metadata = cellxgene_census.get_obs(
    census,
    "homo_sapiens",
    value_filter="is_primary_data == True",
    column_names=["tissue_general"],
)
tissue_counts = tissue_metadata["tissue_general"].value_counts()

Important: Always filter for is_primary_data == True to avoid counting duplicate cells unless specifically analyzing duplicates.

Raw lines 40 to 83
84### 3. Querying Expression Data (Small to Medium Scale)
86For slices whose expression, metadata, and downstream copies fit in memory, use `get_anndata()`:
88```python
89# Basic query with cell type and tissue filters
90adata = cellxgene_census.get_anndata(
91 census=census,
92 organism="Homo sapiens", # or "Mus musculus"
93 obs_value_filter="cell_type == 'B cell' and tissue_general == 'lung' and is_primary_data == True",
94 obs_column_names=["assay", "disease", "sex", "donor_id"],
97# Query specific genes with multiple filters
98adata = cellxgene_census.get_anndata(
99 census=census,
100 organism="Homo sapiens",
101 var_value_filter="feature_name in ['CD4', 'CD8A', 'CD19', 'FOXP3']",
102 obs_value_filter="cell_type == 'T cell' and disease == 'COVID-19' and is_primary_data == True",
103 obs_column_names=["cell_type", "tissue_general", "donor_id"],
105```
107**Filter syntax:**
108- Use `obs_value_filter` for cell filtering
109- Use `var_value_filter` for gene filtering
110- Combine conditions with `and`, `or`
111- Use `in` for multiple values: `tissue in ['lung', 'liver']`
112- Select only needed columns with `obs_column_names`
113- In current LTS releases, `disease` and `disease_ontology_term_id` may contain ` || `-delimited multiple values; inspect available values before relying on exact equality filters for disease cohorts
115**Getting metadata separately:**
116```python
117# Query cell metadata
118cell_metadata = cellxgene_census.get_obs(
119 census, "homo_sapiens",
120 value_filter="disease == 'COVID-19' and is_primary_data == True",
121 column_names=["cell_type", "tissue_general", "donor_id"]
124# Query gene metadata
125gene_metadata = cellxgene_census.get_var(
126 census, "homo_sapiens",
127 value_filter="feature_name in ['CD4', 'CD8A']",
128 column_names=["feature_id", "feature_name", "feature_length"]
130```

3. Querying Expression Data (Small to Medium Scale)

For slices whose expression, metadata, and downstream copies fit in memory, use get_anndata():

# Basic query with cell type and tissue filters
adata = cellxgene_census.get_anndata(
    census=census,
    organism="Homo sapiens",  # or "Mus musculus"
    obs_value_filter="cell_type == 'B cell' and tissue_general == 'lung' and is_primary_data == True",
    obs_column_names=["assay", "disease", "sex", "donor_id"],
)

# Query specific genes with multiple filters
adata = cellxgene_census.get_anndata(
    census=census,
    organism="Homo sapiens",
    var_value_filter="feature_name in ['CD4', 'CD8A', 'CD19', 'FOXP3']",
    obs_value_filter="cell_type == 'T cell' and disease == 'COVID-19' and is_primary_data == True",
    obs_column_names=["cell_type", "tissue_general", "donor_id"],
)

Filter syntax:

  • Use obs_value_filter for cell filtering
  • Use var_value_filter for gene filtering
  • Combine conditions with and, or
  • Use in for multiple values: tissue in ['lung', 'liver']
  • Select only needed columns with obs_column_names
  • In current LTS releases, disease and disease_ontology_term_id may contain || -delimited multiple values; inspect available values before relying on exact equality filters for disease cohorts

Getting metadata separately:

# Query cell metadata
cell_metadata = cellxgene_census.get_obs(
    census, "homo_sapiens",
    value_filter="disease == 'COVID-19' and is_primary_data == True",
    column_names=["cell_type", "tissue_general", "donor_id"]
)

# Query gene metadata
gene_metadata = cellxgene_census.get_var(
    census, "homo_sapiens",
    value_filter="feature_name in ['CD4', 'CD8A']",
    column_names=["feature_id", "feature_name", "feature_length"]
)
Raw lines 84 to 131
132### 4. Large-Scale Queries (Out-of-Core Processing)
134For queries exceeding available RAM, use `axis_query()` with iterative processing:
136```python
137import tiledbsoma as soma
139# Create axis query
140with census["census_data"]["homo_sapiens"].axis_query(
141 measurement_name="RNA",
142 obs_query=soma.AxisQuery(
143 value_filter="tissue_general == 'brain' and is_primary_data == True"
144 ),
145 var_query=soma.AxisQuery(
146 value_filter="feature_name in ['FOXP2', 'TBR1', 'SATB2']"
147 ),
148) as query:
149 # Iterate through expression matrix in chunks
150 iterator = query.X("raw").tables()
151 for batch in iterator:
152 # batch is a pyarrow.Table with columns:
153 # - soma_data: expression value
154 # - soma_dim_0: cell (obs) coordinate
155 # - soma_dim_1: gene (var) coordinate
156 process_batch(batch)
157```
159**Computing incremental statistics:**
161Use the tested per-gene `mean_variance(..., nnz_only=False)` workflow in
162[common_patterns.md](common_patterns.md). Sparse tables contain stored entries,
163so averaging their values silently excludes zeros. Also exclude datasets that did
164not measure a gene; missing measurements are not biological zero expression.

4. Large-Scale Queries (Out-of-Core Processing)

For queries exceeding available RAM, use axis_query() with iterative processing:

import tiledbsoma as soma

# Create axis query
with census["census_data"]["homo_sapiens"].axis_query(
    measurement_name="RNA",
    obs_query=soma.AxisQuery(
        value_filter="tissue_general == 'brain' and is_primary_data == True"
    ),
    var_query=soma.AxisQuery(
        value_filter="feature_name in ['FOXP2', 'TBR1', 'SATB2']"
    ),
) as query:
    # Iterate through expression matrix in chunks
    iterator = query.X("raw").tables()
    for batch in iterator:
        # batch is a pyarrow.Table with columns:
        # - soma_data: expression value
        # - soma_dim_0: cell (obs) coordinate
        # - soma_dim_1: gene (var) coordinate
        process_batch(batch)

Computing incremental statistics:

Use the tested per-gene mean_variance(..., nnz_only=False) workflow in common_patterns.md. Sparse tables contain stored entries, so averaging their values silently excludes zeros. Also exclude datasets that did not measure a gene; missing measurements are not biological zero expression.

Raw lines 132 to 165
166### 5. Machine Learning with PyTorch
168For training models, use TileDB-SOMA-ML. The former `cellxgene_census.experimental.ml` PyTorch loaders are absent from 1.18.0.
170```python
171import tiledbsoma as soma
172import torch
173from tiledbsoma_ml import ExperimentDataset, experiment_dataloader
175with cellxgene_census.open_soma(census_version="2025-11-08") as census:
176 experiment = census["census_data"]["homo_sapiens"]
177 with experiment.axis_query(
178 measurement_name="RNA",
179 obs_query=soma.AxisQuery(
180 value_filter="tissue_general == 'liver' and is_primary_data == True"
181 ),
182 ) as query:
183 dataset = ExperimentDataset(
184 query=query,
185 layer_name="raw",
186 obs_column_names=["cell_type"],
187 batch_size=128,
188 shuffle=True,
189 )
190 dataloader = experiment_dataloader(dataset)
192 # Illustrative: fit label_to_id on training metadata; supply model,
193 # criterion, optimizer, and num_epochs for the intended task.
194 # Training loop
195 for epoch in range(num_epochs):
196 dataset.set_epoch(epoch)
197 for X, obs in dataloader:
198 encoded = obs["cell_type"].astype(str).map(label_to_id)
199 if encoded.isna().any():
200 raise ValueError("Unseen cell type in training batch")
201 labels = torch.as_tensor(encoded.to_numpy(), dtype=torch.long)
203 # Forward pass (default loader emits NumPy X)
204 outputs = model(torch.as_tensor(X, dtype=torch.float32))
205 loss = criterion(outputs, labels)
207 # Backward pass
208 optimizer.zero_grad()
209 loss.backward()
210 optimizer.step()
211```
213**Train/test splitting:**
214```python
215train_dataset, test_dataset = dataset.random_split(0.8, 0.2, seed=42)
216train_loader = experiment_dataloader(train_dataset, num_workers=0)
217test_loader = experiment_dataloader(test_dataset, num_workers=0)
218```
220The split block belongs inside the open query context. Cell-level random splits
221do not test generalization to unseen donors/studies. Keep a fixed training label
222vocabulary; use donor/study holdouts for that evaluation.
224Use `batch_size` and `shuffle` on `ExperimentDataset`, not on `torch.utils.data.DataLoader`; `experiment_dataloader()` rejects DataLoader-level `batch_size`, `shuffle`, `sampler`, and `batch_sampler` arguments.

5. Machine Learning with PyTorch

For training models, use TileDB-SOMA-ML. The former cellxgene_census.experimental.ml PyTorch loaders are absent from 1.18.0.

import tiledbsoma as soma
import torch
from tiledbsoma_ml import ExperimentDataset, experiment_dataloader

with cellxgene_census.open_soma(census_version="2025-11-08") as census:
    experiment = census["census_data"]["homo_sapiens"]
    with experiment.axis_query(
        measurement_name="RNA",
        obs_query=soma.AxisQuery(
            value_filter="tissue_general == 'liver' and is_primary_data == True"
        ),
    ) as query:
        dataset = ExperimentDataset(
            query=query,
            layer_name="raw",
            obs_column_names=["cell_type"],
            batch_size=128,
            shuffle=True,
        )
        dataloader = experiment_dataloader(dataset)

        # Illustrative: fit label_to_id on training metadata; supply model,
        # criterion, optimizer, and num_epochs for the intended task.
        # Training loop
        for epoch in range(num_epochs):
            dataset.set_epoch(epoch)
            for X, obs in dataloader:
                encoded = obs["cell_type"].astype(str).map(label_to_id)
                if encoded.isna().any():
                    raise ValueError("Unseen cell type in training batch")
                labels = torch.as_tensor(encoded.to_numpy(), dtype=torch.long)

                # Forward pass (default loader emits NumPy X)
                outputs = model(torch.as_tensor(X, dtype=torch.float32))
                loss = criterion(outputs, labels)

                # Backward pass
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

Train/test splitting:

train_dataset, test_dataset = dataset.random_split(0.8, 0.2, seed=42)
train_loader = experiment_dataloader(train_dataset, num_workers=0)
test_loader = experiment_dataloader(test_dataset, num_workers=0)

The split block belongs inside the open query context. Cell-level random splits do not test generalization to unseen donors/studies. Keep a fixed training label vocabulary; use donor/study holdouts for that evaluation.

Use batch_size and shuffle on ExperimentDataset, not on torch.utils.data.DataLoader; experiment_dataloader() rejects DataLoader-level batch_size, shuffle, sampler, and batch_sampler arguments.

Raw lines 166 to 225
226### 6. Spatial Census Data
228Spatial data is available for supported Census releases in a separate `census_spatial_sequencing` collection. Use the spatial extra and a current TileDB-SOMA version when querying Visium or Slide-seq V2 data:
230```python
231import cellxgene_census
232import tiledbsoma as soma
234with cellxgene_census.open_soma(census_version="2025-11-08") as census:
235 spatial_experiment = census["census_spatial_sequencing"]["homo_sapiens"]
236 with spatial_experiment.axis_query(
237 measurement_name="RNA",
238 obs_query=soma.AxisQuery(
239 value_filter="dataset_id == '4cceac62-9513-42a4-90e5-2878dbb0192c'"
240 ),
241 ) as query:
242 sdata = query.to_spatialdata(X_name="raw")
243```

6. Spatial Census Data

Spatial data is available for supported Census releases in a separate census_spatial_sequencing collection. Use the spatial extra and a current TileDB-SOMA version when querying Visium or Slide-seq V2 data:

import cellxgene_census
import tiledbsoma as soma

with cellxgene_census.open_soma(census_version="2025-11-08") as census:
    spatial_experiment = census["census_spatial_sequencing"]["homo_sapiens"]
    with spatial_experiment.axis_query(
        measurement_name="RNA",
        obs_query=soma.AxisQuery(
            value_filter="dataset_id == '4cceac62-9513-42a4-90e5-2878dbb0192c'"
        ),
    ) as query:
        sdata = query.to_spatialdata(X_name="raw")
Raw lines 226 to 244
245### 7. Integration with Scanpy
247Seamlessly integrate Census data with scanpy workflows:
249```python
250import scanpy as sc
252# Load data from Census
253adata = cellxgene_census.get_anndata(
254 census=census,
255 organism="Homo sapiens",
256 obs_value_filter="cell_type == 'neuron' and tissue_general == 'brain' and is_primary_data == True",
259# Standard scanpy workflow
260sc.pp.normalize_total(adata, target_sum=1e4)
261sc.pp.log1p(adata)
262sc.pp.highly_variable_genes(adata, n_top_genes=2000)
264# Dimensionality reduction
265sc.pp.pca(adata, n_comps=50)
266sc.pp.neighbors(adata)
267sc.tl.umap(adata)
269# Visualization
270sc.pl.umap(adata, color=["cell_type", "tissue", "disease"])
271```

7. Integration with Scanpy

Seamlessly integrate Census data with scanpy workflows:

import scanpy as sc

# Load data from Census
adata = cellxgene_census.get_anndata(
    census=census,
    organism="Homo sapiens",
    obs_value_filter="cell_type == 'neuron' and tissue_general == 'brain' and is_primary_data == True",
)

# Standard scanpy workflow
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
sc.pp.highly_variable_genes(adata, n_top_genes=2000)

# Dimensionality reduction
sc.pp.pca(adata, n_comps=50)
sc.pp.neighbors(adata)
sc.tl.umap(adata)

# Visualization
sc.pl.umap(adata, color=["cell_type", "tissue", "disease"])
Raw lines 245 to 272
273### 8. Multi-Dataset Integration
275Query and integrate multiple datasets:
277```python
278# Strategy 1: Query multiple tissues separately
279tissues = ["lung", "liver", "kidney"]
280adatas = []
282for tissue in tissues:
283 adata = cellxgene_census.get_anndata(
284 census=census,
285 organism="Homo sapiens",
286 obs_value_filter=f"tissue_general == '{tissue}' and is_primary_data == True",
287 )
288 adata.obs["query_tissue"] = tissue
289 adatas.append(adata)
291# Concatenate with AnnData's current API
292import anndata as ad
293for adata in adatas:
294 adata.var_names = adata.var["feature_id"].astype(str)
295combined = ad.concat(adatas, label="query_tissue", keys=tissues, join="inner")
296# Concatenation preserves batches; it does not correct batch effects.
298# Strategy 2: Query multiple datasets directly
299adata = cellxgene_census.get_anndata(
300 census=census,
301 organism="Homo sapiens",
302 obs_value_filter="tissue_general in ['lung', 'liver', 'kidney'] and is_primary_data == True",
304```

8. Multi-Dataset Integration

Query and integrate multiple datasets:

# Strategy 1: Query multiple tissues separately
tissues = ["lung", "liver", "kidney"]
adatas = []

for tissue in tissues:
    adata = cellxgene_census.get_anndata(
        census=census,
        organism="Homo sapiens",
        obs_value_filter=f"tissue_general == '{tissue}' and is_primary_data == True",
    )
    adata.obs["query_tissue"] = tissue
    adatas.append(adata)

# Concatenate with AnnData's current API
import anndata as ad
for adata in adatas:
    adata.var_names = adata.var["feature_id"].astype(str)
combined = ad.concat(adatas, label="query_tissue", keys=tissues, join="inner")
# Concatenation preserves batches; it does not correct batch effects.

# Strategy 2: Query multiple datasets directly
adata = cellxgene_census.get_anndata(
    census=census,
    organism="Homo sapiens",
    obs_value_filter="tissue_general in ['lung', 'liver', 'kidney'] and is_primary_data == True",
)
Raw lines 273 to 304