Multi-Batch Analysis

Mievformer learns the relationship between continuous cell states and local tissue contexts across multiple spatial slices. Sample-conditioned modeling measures cell-state enrichment relative to each slice’s background distribution, while spatial neighborhoods remain within their source slice. The resulting shared representation supports niche identification across samples.

The executable example is run_multibatch_tutorial.py and does not download external data.

Prepare the AnnData object

Concatenate slices into one AnnData object and provide an obs column that identifies their source. Cells do not need to be grouped by sample; the API orders them internally and restores the original order before returning.

import numpy as np
import pandas as pd
import scanpy as sc

rng = np.random.default_rng(11)
cells_per_slice = 60
labels = np.repeat(['slice_A', 'slice_B'], cells_per_slice)
spatial = np.vstack([
    rng.uniform(0, 8, size=(cells_per_slice, 2)),
    rng.uniform(0, 8, size=(cells_per_slice, 2)),
])
counts = rng.poisson(1.5, size=(120, 30)).astype(np.float32)
u, s, _ = np.linalg.svd(
    counts - counts.mean(axis=0, keepdims=True), full_matrices=False
)

# Interleave the two slices to demonstrate order restoration.
order = np.ravel(np.column_stack([
    np.arange(cells_per_slice),
    np.arange(cells_per_slice, 2 * cells_per_slice),
]))
adata = sc.AnnData(
    counts[order],
    obs=pd.DataFrame(
        {'sample': labels[order]},
        index=[f'cell_{i:03d}' for i in range(120)],
    ),
)
adata.obsm['spatial'] = spatial[order].astype(np.float32)
adata.obsm['X_pca'] = (u[:, :10] * s[:10])[order].astype(np.float32)

Coordinates from different slices may use the same numeric range. Mievformer never interprets them as adjacent when batch_key is supplied.

Learn microenvironments across slices

Supply the sample column during training. The default batch_correct='auto' enables sample conditioning when that column contains multiple samples:

import torch
import mievformer as mf

original_order = adata.obs_names.copy()
adata = mf.optimize_nicheformer(
    adata,
    model_path='multibatch_model.pth',
    batch_key='sample',
    batch_correct='auto',
    ngpu=1 if torch.cuda.is_available() else 0,
    max_epochs=1,
    batch_size=32,
    neighbor_num=5,
    latent_dim=6,
    ca_reference_num=30,
    ca_n_components=5,
    niche_n_neighbors=10,
    random_state=11,
)

assert adata.obs_names.equals(original_order)

With production-sized data, omit ca_reference_num and ca_n_components to use adaptive reference sampling and automatic dimension selection for the niche representation. Increase max_epochs and choose neighbor_num for the spatial scale of interest.

Explore shared niches and sample differences

Each local context is evaluated against reference cell states balanced across samples. Profiles are normalized within each sample and each sample contributes equal total weight to the shared representation. The result includes obsm['e'], obsm['X_umap'], and obs['leiden_e'], as in the single-slice workflow.

Compare sample labels and inferred niches:

sc.pl.umap(adata, color=['sample', 'leiden_e'])

With real data, add available cell-type or tissue-region annotations to the plot to assess biological structure. The synthetic example above contains only sample labels.

Retain sample information for reproducibility

The result also contains the sample information used by the model:

AnnData key

Meaning

obsm['batch_one_hot']

Persisted sample conditioning used by the model

uns['mievformer_batch_contract']

Sample key, first-observed order, and counts

Keep this metadata with the fitted model when reusing results. If the required model or sample-conditioning metadata is unavailable, the standard workflow raises an error. The API reference documents the stored representation and its processing options.