Skill Tracer
Skills › Scientific Agent Skills › anndata › concatenation.md

concatenation.md

421 lines · skills/anndata/references/concatenation.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Concatenating AnnData Objects
2
3Combine multiple AnnData objects along either axis. Reviewed for AnnData 0.13.4. Match stable identifiers, species/genome build, units, and preprocessing before concatenating. Feature unions with sparse zero fill conflate unmeasured features with observed zeros; preserve a per-batch feature-presence record when that distinction matters.
4

Concatenating AnnData Objects

Combine multiple AnnData objects along either axis. Reviewed for AnnData 0.13.4. Match stable identifiers, species/genome build, units, and preprocessing before concatenating. Feature unions with sparse zero fill conflate unmeasured features with observed zeros; preserve a per-batch feature-presence record when that distinction matters.

Raw lines 1 to 4
5## Basic Concatenation
6

Basic Concatenation

Raw lines 5 to 6
7### Concatenate along observations (stack cells/samples)
8```python
9import anndata as ad
10import numpy as np
12# Create multiple AnnData objects
13adata1 = ad.AnnData(X=np.random.rand(100, 50))
14adata2 = ad.AnnData(X=np.random.rand(150, 50))
15adata3 = ad.AnnData(X=np.random.rand(200, 50))
17# Concatenate along observations (axis=0, default)
18adata_combined = ad.concat([adata1, adata2, adata3], axis=0, index_unique='-')
20print(adata_combined.shape) # (450, 50)
21```

Concatenate along observations (stack cells/samples)

import anndata as ad
import numpy as np

# Create multiple AnnData objects
adata1 = ad.AnnData(X=np.random.rand(100, 50))
adata2 = ad.AnnData(X=np.random.rand(150, 50))
adata3 = ad.AnnData(X=np.random.rand(200, 50))

# Concatenate along observations (axis=0, default)
adata_combined = ad.concat([adata1, adata2, adata3], axis=0, index_unique='-')

print(adata_combined.shape)  # (450, 50)
Raw lines 7 to 22
23### Concatenate along variables (stack genes/features)
24```python
25# Create objects with same observations, different variables
26adata1 = ad.AnnData(X=np.random.rand(100, 50))
27adata2 = ad.AnnData(X=np.random.rand(100, 30))
28adata3 = ad.AnnData(X=np.random.rand(100, 70))
30# Concatenate along variables (axis=1)
31adata_combined = ad.concat([adata1, adata2, adata3], axis=1)
33print(adata_combined.shape) # (100, 150)
34```

Concatenate along variables (stack genes/features)

# Create objects with same observations, different variables
adata1 = ad.AnnData(X=np.random.rand(100, 50))
adata2 = ad.AnnData(X=np.random.rand(100, 30))
adata3 = ad.AnnData(X=np.random.rand(100, 70))

# Concatenate along variables (axis=1)
adata_combined = ad.concat([adata1, adata2, adata3], axis=1)

print(adata_combined.shape)  # (100, 150)
Raw lines 23 to 35
36## Join Types
38### Inner join (intersection)
39Keep only variables/observations present in all objects.
41```python
42import pandas as pd
44# Create objects with different variables
45adata1 = ad.AnnData(
46 X=np.random.rand(100, 50),
47 var=pd.DataFrame(index=[f'Gene_{i}' for i in range(50)])
49adata2 = ad.AnnData(
50 X=np.random.rand(150, 60),
51 var=pd.DataFrame(index=[f'Gene_{i}' for i in range(10, 70)])
54# Inner join: only genes 10-49 are kept (overlap)
55adata_inner = ad.concat([adata1, adata2], join='inner')
56print(adata_inner.n_vars) # 40 genes (overlap)
57```

Inner join (intersection)

Keep only variables/observations present in all objects.

import pandas as pd

# Create objects with different variables
adata1 = ad.AnnData(
    X=np.random.rand(100, 50),
    var=pd.DataFrame(index=[f'Gene_{i}' for i in range(50)])
)
adata2 = ad.AnnData(
    X=np.random.rand(150, 60),
    var=pd.DataFrame(index=[f'Gene_{i}' for i in range(10, 70)])
)

# Inner join: only genes 10-49 are kept (overlap)
adata_inner = ad.concat([adata1, adata2], join='inner')
print(adata_inner.n_vars)  # 40 genes (overlap)
Raw lines 38 to 58
59### Outer join (union)
60Keep all variables/observations, filling missing values.
62```python
63# Outer join: all genes are kept
64adata_outer = ad.concat([adata1, adata2], join='outer')
65print(adata_outer.n_vars) # 70 genes (union)
67# Missing values are filled with appropriate defaults:
68# - 0 for sparse matrices
69# - NaN for dense matrices
70```

Outer join (union)

Keep all variables/observations, filling missing values.

# Outer join: all genes are kept
adata_outer = ad.concat([adata1, adata2], join='outer')
print(adata_outer.n_vars)  # 70 genes (union)

# Missing values are filled with appropriate defaults:
# - 0 for sparse matrices
# - NaN for dense matrices
Raw lines 59 to 71
72### Fill values for outer joins
73```python
74# Specify fill value for missing data
75adata_filled = ad.concat([adata1, adata2], join='outer', fill_value=0)
76```

Fill values for outer joins

# Specify fill value for missing data
adata_filled = ad.concat([adata1, adata2], join='outer', fill_value=0)
Raw lines 72 to 77
78## Tracking Data Sources

Tracking Data Sources

Raw lines 78 to 79
80### Add batch labels
81```python
82# Label which object each observation came from
83adata_combined = ad.concat(
84 [adata1, adata2, adata3],
85 label='batch', # Column name for labels
86 keys=['batch1', 'batch2', 'batch3'] # Labels for each object
89print(adata_combined.obs['batch'].value_counts())
90# batch1 100
91# batch2 150
92# batch3 200
93```

Add batch labels

# Label which object each observation came from
adata_combined = ad.concat(
    [adata1, adata2, adata3],
    label='batch',  # Column name for labels
    keys=['batch1', 'batch2', 'batch3']  # Labels for each object
)

print(adata_combined.obs['batch'].value_counts())
# batch1    100
# batch2    150
# batch3    200
Raw lines 80 to 94
95### Automatic batch labels
96```python
97# If keys not provided, uses integer indices
98adata_combined = ad.concat(
99 [adata1, adata2, adata3],
100 label='dataset'
102# dataset column contains: 0, 1, 2
103```

Automatic batch labels

# If keys not provided, uses integer indices
adata_combined = ad.concat(
    [adata1, adata2, adata3],
    label='dataset'
)
# dataset column contains: 0, 1, 2
Raw lines 95 to 104
105## Merge Strategies
107Control how metadata from different objects is combined using the `merge` parameter.

Merge Strategies

Control how metadata from different objects is combined using the merge parameter.

Raw lines 105 to 108
109### merge=None (default for either axis)
110Exclude metadata on non-concatenation axis.
112```python
113# When concatenating observations, merge controls var-side annotations
114adata1.var['gene_type'] = 'protein_coding'
115adata2.var['gene_type'] = 'protein_coding'
117# Variable index is kept; var annotation columns are dropped even if identical
118adata_combined = ad.concat([adata1, adata2], merge=None)
119```

merge=None (default for either axis)

Exclude metadata on non-concatenation axis.

# When concatenating observations, merge controls var-side annotations
adata1.var['gene_type'] = 'protein_coding'
adata2.var['gene_type'] = 'protein_coding'

# Variable index is kept; var annotation columns are dropped even if identical
adata_combined = ad.concat([adata1, adata2], merge=None)
Raw lines 109 to 120
121### merge='same'
122Keep metadata that is identical across all objects.
124```python
125adata1.var['chromosome'] = 'chr1'
126adata2.var['chromosome'] = 'chr1'
127adata1.var['type'] = 'protein_coding'
128adata2.var['type'] = 'lncRNA' # Different
130# 'chromosome' is kept (same), 'type' is excluded (different)
131adata_combined = ad.concat([adata1, adata2], merge='same')
132```

merge='same'

Keep metadata that is identical across all objects.

adata1.var['chromosome'] = 'chr1'
adata2.var['chromosome'] = 'chr1'
adata1.var['type'] = 'protein_coding'
adata2.var['type'] = 'lncRNA'  # Different

# 'chromosome' is kept (same), 'type' is excluded (different)
adata_combined = ad.concat([adata1, adata2], merge='same')
Raw lines 121 to 133
134### merge='unique'
135Keep elements with one possible value across inputs after index alignment; this does not mean all entries within a column must be identical.
137```python
138adata1.var['gene_id'] = adata1.var_names
139adata2.var['gene_id'] = adata2.var_names
141# gene_id is kept (unique values for each key)
142adata_combined = ad.concat([adata1, adata2], merge='unique')
143```

merge='unique'

Keep elements with one possible value across inputs after index alignment; this does not mean all entries within a column must be identical.

adata1.var['gene_id'] = adata1.var_names
adata2.var['gene_id'] = adata2.var_names

# gene_id is kept (unique values for each key)
adata_combined = ad.concat([adata1, adata2], merge='unique')
Raw lines 134 to 144
145### merge='first'
146Take values from the first object containing each key.
148```python
149adata1.var['description'] = 'Desc1'
150adata2.var['description'] = 'Desc2'
152# Uses descriptions from adata1
153adata_combined = ad.concat([adata1, adata2], merge='first')
154```

merge='first'

Take values from the first object containing each key.

adata1.var['description'] = 'Desc1'
adata2.var['description'] = 'Desc2'

# Uses descriptions from adata1
adata_combined = ad.concat([adata1, adata2], merge='first')
Raw lines 145 to 155
156### merge='only'
157Keep metadata that appears in only one object.
159```python
160adata1.var['adata1_specific'] = 1
161adata2.var['adata2_specific'] = 2
163# Both metadata columns are kept
164adata_combined = ad.concat([adata1, adata2], merge='only')
165```

merge='only'

Keep metadata that appears in only one object.

adata1.var['adata1_specific'] = 1
adata2.var['adata2_specific'] = 2

# Both metadata columns are kept
adata_combined = ad.concat([adata1, adata2], merge='only')
Raw lines 156 to 166
167## Handling Index Conflicts

Handling Index Conflicts

Raw lines 167 to 168
169### Make indices unique
170```python
171import pandas as pd
173# Create objects with overlapping observation names
174adata1 = ad.AnnData(
175 X=np.random.rand(3, 10),
176 obs=pd.DataFrame(index=['cell_1', 'cell_2', 'cell_3'])
178adata2 = ad.AnnData(
179 X=np.random.rand(3, 10),
180 obs=pd.DataFrame(index=['cell_1', 'cell_2', 'cell_3'])
183# Make indices unique by appending batch keys
184adata_combined = ad.concat(
185 [adata1, adata2],
186 label='batch',
187 keys=['batch1', 'batch2'],
188 index_unique='_' # Separator for making indices unique
191print(adata_combined.obs_names)
192# ['cell_1_batch1', 'cell_2_batch1', 'cell_3_batch1',
193# 'cell_1_batch2', 'cell_2_batch2', 'cell_3_batch2']
194```

Make indices unique

import pandas as pd

# Create objects with overlapping observation names
adata1 = ad.AnnData(
    X=np.random.rand(3, 10),
    obs=pd.DataFrame(index=['cell_1', 'cell_2', 'cell_3'])
)
adata2 = ad.AnnData(
    X=np.random.rand(3, 10),
    obs=pd.DataFrame(index=['cell_1', 'cell_2', 'cell_3'])
)

# Make indices unique by appending batch keys
adata_combined = ad.concat(
    [adata1, adata2],
    label='batch',
    keys=['batch1', 'batch2'],
    index_unique='_'  # Separator for making indices unique
)

print(adata_combined.obs_names)
# ['cell_1_batch1', 'cell_2_batch1', 'cell_3_batch1',
#  'cell_1_batch2', 'cell_2_batch2', 'cell_3_batch2']
Raw lines 169 to 195
196## Concatenating Layers
198```python
199# Objects with layers
200adata1 = ad.AnnData(X=np.random.rand(100, 50))
201adata1.layers['normalized'] = np.random.rand(100, 50)
202adata1.layers['scaled'] = np.random.rand(100, 50)
204adata2 = ad.AnnData(X=np.random.rand(150, 50))
205adata2.layers['normalized'] = np.random.rand(150, 50)
206adata2.layers['scaled'] = np.random.rand(150, 50)
208# With join="inner", common layers are concatenated (outer joins union keys)
209adata_combined = ad.concat([adata1, adata2])
211print(adata_combined.layers.keys())
212# Contains None (X), 'normalized', and 'scaled' in AnnData 0.13.4
213```

Concatenating Layers

# Objects with layers
adata1 = ad.AnnData(X=np.random.rand(100, 50))
adata1.layers['normalized'] = np.random.rand(100, 50)
adata1.layers['scaled'] = np.random.rand(100, 50)

adata2 = ad.AnnData(X=np.random.rand(150, 50))
adata2.layers['normalized'] = np.random.rand(150, 50)
adata2.layers['scaled'] = np.random.rand(150, 50)

# With join="inner", common layers are concatenated (outer joins union keys)
adata_combined = ad.concat([adata1, adata2])

print(adata_combined.layers.keys())
# Contains None (X), 'normalized', and 'scaled' in AnnData 0.13.4
Raw lines 196 to 214
215## Concatenating Multi-dimensional Annotations

Concatenating Multi-dimensional Annotations

Raw lines 215 to 216
217### obsm/varm
218```python
219# Objects with embeddings
220adata1.obsm['X_pca'] = np.random.rand(100, 50)
221adata2.obsm['X_pca'] = np.random.rand(150, 50)
223# obsm is concatenated along observation axis
224adata_combined = ad.concat([adata1, adata2])
225print(adata_combined.obsm['X_pca'].shape) # (250, 50)
226```

obsm/varm

# Objects with embeddings
adata1.obsm['X_pca'] = np.random.rand(100, 50)
adata2.obsm['X_pca'] = np.random.rand(150, 50)

# obsm is concatenated along observation axis
adata_combined = ad.concat([adata1, adata2])
print(adata_combined.obsm['X_pca'].shape)  # (250, 50)
Raw lines 217 to 227
228### obsp/varp (pairwise annotations)
229```python
230from scipy.sparse import csr_matrix
232# Pairwise matrices
233adata1.obsp['connectivities'] = csr_matrix((100, 100))
234adata2.obsp['connectivities'] = csr_matrix((150, 150))
236# By default, obsp is NOT concatenated (set pairwise=True to include)
237adata_combined = ad.concat([adata1, adata2])
238# adata_combined.obsp is empty
240# Include pairwise data (creates block diagonal matrix)
241adata_combined = ad.concat([adata1, adata2], pairwise=True)
242print(adata_combined.obsp['connectivities'].shape) # (250, 250)
243```

obsp/varp (pairwise annotations)

from scipy.sparse import csr_matrix

# Pairwise matrices
adata1.obsp['connectivities'] = csr_matrix((100, 100))
adata2.obsp['connectivities'] = csr_matrix((150, 150))

# By default, obsp is NOT concatenated (set pairwise=True to include)
adata_combined = ad.concat([adata1, adata2])
# adata_combined.obsp is empty

# Include pairwise data (creates block diagonal matrix)
adata_combined = ad.concat([adata1, adata2], pairwise=True)
print(adata_combined.obsp['connectivities'].shape)  # (250, 250)
Raw lines 228 to 244
245## Concatenating uns (unstructured)
247Unstructured metadata is merged recursively:
249```python
250adata1.uns['experiment'] = {'date': '2025-01-01', 'batch': 'A'}
251adata2.uns['experiment'] = {'date': '2025-01-01', 'batch': 'B'}
253# Using merge='unique' for uns
254adata_combined = ad.concat([adata1, adata2], uns_merge='unique')
255# 'date' is kept; conflicting 'batch' is excluded
256```

Concatenating uns (unstructured)

Unstructured metadata is merged recursively:

adata1.uns['experiment'] = {'date': '2025-01-01', 'batch': 'A'}
adata2.uns['experiment'] = {'date': '2025-01-01', 'batch': 'B'}

# Using merge='unique' for uns
adata_combined = ad.concat([adata1, adata2], uns_merge='unique')
# 'date' is kept; conflicting 'batch' is excluded
Raw lines 245 to 257
258## Lazy Concatenation (AnnCollection)
260For very large datasets, use `AnnCollection` to lazily concatenate AnnData objects along the observation axis. This API is experimental; use backed AnnData objects when the inputs are stored in `.h5ad` files.
262```python
263import anndata as ad
264from anndata.experimental import AnnCollection
266files = ['data1.h5ad', 'data2.h5ad', 'data3.h5ad']
267backed_adatas = [ad.read_h5ad(path, backed='r') for path in files]
269collection = AnnCollection(
270 backed_adatas,
271 join_obs='outer',
272 join_vars='inner',
273 label='dataset',
274 keys=['dataset1', 'dataset2', 'dataset3']
277# Access data lazily
278print(collection.n_obs) # Total observations
279print(collection.obs.head()) # Metadata loaded, not X
281# Collection conversion contains annotations only; X is None.
282metadata_only = collection.to_adata()
284# A selected view can gather X, but 0.13.4 default conversion may fail because
285# X also appears in layers[None]. This workaround EXPLICITLY omits all named layers.
286selected = collection[:100, :].to_adata(ignore_layers=True)
287# This is not a full-fidelity conversion (var metadata/raw/uns are not restored).
288```

Lazy Concatenation (AnnCollection)

For very large datasets, use AnnCollection to lazily concatenate AnnData objects along the observation axis. This API is experimental; use backed AnnData objects when the inputs are stored in .h5ad files.

import anndata as ad
from anndata.experimental import AnnCollection

files = ['data1.h5ad', 'data2.h5ad', 'data3.h5ad']
backed_adatas = [ad.read_h5ad(path, backed='r') for path in files]

collection = AnnCollection(
    backed_adatas,
    join_obs='outer',
    join_vars='inner',
    label='dataset',
    keys=['dataset1', 'dataset2', 'dataset3']
)

# Access data lazily
print(collection.n_obs)  # Total observations
print(collection.obs.head())  # Metadata loaded, not X

# Collection conversion contains annotations only; X is None.
metadata_only = collection.to_adata()

# A selected view can gather X, but 0.13.4 default conversion may fail because
# X also appears in layers[None]. This workaround EXPLICITLY omits all named layers.
selected = collection[:100, :].to_adata(ignore_layers=True)
# This is not a full-fidelity conversion (var metadata/raw/uns are not restored).
Raw lines 258 to 289
290### Working with AnnCollection
291```python
292# Subset without loading data
293subset = collection[collection.obs['cell_type'] == 'T cell']
295# Indexing is by observations/variables, not source dataset.
296first_observation = collection[0, :]
297# Keep the original input list to iterate through source datasets.
298for source in backed_adatas:
299 print(source.shape)
300# After materializing needed subsets:
301for source in backed_adatas:
302 source.file.close()
303```

Working with AnnCollection

# Subset without loading data
subset = collection[collection.obs['cell_type'] == 'T cell']

# Indexing is by observations/variables, not source dataset.
first_observation = collection[0, :]
# Keep the original input list to iterate through source datasets.
for source in backed_adatas:
    print(source.shape)
# After materializing needed subsets:
for source in backed_adatas:
    source.file.close()
Raw lines 290 to 304
305## Concatenation on Disk
307For datasets too large for memory, use experimental `concat_on_disk`.
308`max_loaded_elems` limits sparse processing; dense input uses Dask. Pairwise
309concatenation is not implemented here. Reopen the result and compare it with
310`ad.concat` on a representative subset before committing a large workflow:
312```python
313import anndata as ad
314from anndata.experimental import concat_on_disk
316# Concatenate without loading into memory
317concat_on_disk(
318 ['data1.h5ad', 'data2.h5ad', 'data3.h5ad'],
319 'combined.h5ad',
320 join='outer',
321 label='source',
322 keys=['one', 'two', 'three'],
323 index_unique='-',
324 max_loaded_elems=1_000_000
327# Load result in backed mode
328adata = ad.read_h5ad('combined.h5ad', backed='r')
329```

Concatenation on Disk

For datasets too large for memory, use experimental concat_on_disk. max_loaded_elems limits sparse processing; dense input uses Dask. Pairwise concatenation is not implemented here. Reopen the result and compare it with ad.concat on a representative subset before committing a large workflow:

import anndata as ad
from anndata.experimental import concat_on_disk

# Concatenate without loading into memory
concat_on_disk(
    ['data1.h5ad', 'data2.h5ad', 'data3.h5ad'],
    'combined.h5ad',
    join='outer',
    label='source',
    keys=['one', 'two', 'three'],
    index_unique='-',
    max_loaded_elems=1_000_000
)

# Load result in backed mode
adata = ad.read_h5ad('combined.h5ad', backed='r')
Raw lines 305 to 330
331## Common Concatenation Patterns

Common Concatenation Patterns

Raw lines 331 to 332
333### Combine technical replicates
334```python
335# Multiple runs of the same samples
336replicates = [adata_run1, adata_run2, adata_run3]
337adata_combined = ad.concat(
338 replicates,
339 label='technical_replicate',
340 keys=['rep1', 'rep2', 'rep3'],
341 join='inner' # Keep only genes measured in all runs
343```

Combine technical replicates

# Multiple runs of the same samples
replicates = [adata_run1, adata_run2, adata_run3]
adata_combined = ad.concat(
    replicates,
    label='technical_replicate',
    keys=['rep1', 'rep2', 'rep3'],
    join='inner'  # Keep only genes measured in all runs
)
Raw lines 333 to 344
345### Combine batches from experiment
346```python
347# Different experimental batches
348batches = [adata_batch1, adata_batch2, adata_batch3]
349adata_combined = ad.concat(
350 batches,
351 label='batch',
352 keys=['batch1', 'batch2', 'batch3'],
353 join='outer' # Keep all genes
356# Later: apply batch correction
357```

Combine batches from experiment

# Different experimental batches
batches = [adata_batch1, adata_batch2, adata_batch3]
adata_combined = ad.concat(
    batches,
    label='batch',
    keys=['batch1', 'batch2', 'batch3'],
    join='outer'  # Keep all genes
)

# Later: apply batch correction
Raw lines 345 to 358
359### Merge multi-modal data
360```python
361# Different measurement modalities (e.g., RNA + protein)
362adata_rna = ad.AnnData(X=np.random.rand(100, 2000))
363adata_protein = ad.AnnData(X=np.random.rand(100, 50))
365# Concatenate along variables to combine modalities
366adata_multimodal = ad.concat([adata_rna, adata_protein], axis=1)
368# Add labels to distinguish modalities
369adata_multimodal.var['modality'] = ['RNA'] * 2000 + ['protein'] * 50
370```

Merge multi-modal data

# Different measurement modalities (e.g., RNA + protein)
adata_rna = ad.AnnData(X=np.random.rand(100, 2000))
adata_protein = ad.AnnData(X=np.random.rand(100, 50))

# Concatenate along variables to combine modalities
adata_multimodal = ad.concat([adata_rna, adata_protein], axis=1)

# Add labels to distinguish modalities
adata_multimodal.var['modality'] = ['RNA'] * 2000 + ['protein'] * 50
Raw lines 359 to 371
372## Best Practices
3741. **Check compatibility before concatenating**
375```python
376# Verify shapes are compatible
377print([adata.n_vars for adata in [adata1, adata2, adata3]])
379# Check variable names match
380print([set(adata.var_names) for adata in [adata1, adata2, adata3]])
381```
3832. **Use appropriate join type**
384- `inner`: When you need the same features across all samples (most stringent)
385- `outer`: When you want to preserve all features (most inclusive)
3873. **Track data sources**
388Always use `label` and `keys` to track which observations came from which dataset.
3904. **Consider memory usage**
391- For large datasets, use `AnnCollection` or `concat_on_disk`
392- Consider backed mode for the result
3945. **Handle batch effects**
395Concatenation does not estimate batch effects. Choose an analysis method based on
396experimental design and matrix semantics; automatic correction can erase biology
397when batch and condition are confounded. See the scanpy skill for analysis.
3996. **Validate results**
400```python
401# Check dimensions
402print(adata_combined.shape)
404# Check batch distribution
405print(adata_combined.obs['batch'].value_counts())
407# Verify metadata integrity
408print(adata_combined.var.head())
409print(adata_combined.obs.head())
410```
412`pairwise=True` creates block-diagonal graphs with no cross-batch neighbors; recompute
413a graph after integration when cross-batch relationships are needed. `axis=1` aligns
414observations by name but does not make RNA counts and protein values statistically
415comparable; preserve modality and use a MuData container for separate matrices.
417Sources: [concat](https://anndata.readthedocs.io/en/stable/generated/anndata.concat.html),
418[AnnCollection](https://anndata.readthedocs.io/en/stable/generated/anndata.experimental.AnnCollection.html),
419[to_adata](https://anndata.readthedocs.io/en/stable/generated/anndata.experimental.AnnCollection.to_adata.html),
420[concat_on_disk](https://anndata.readthedocs.io/en/stable/generated/anndata.experimental.concat_on_disk.html),
421and installed 0.13.4 source. Biological and user-file examples are illustrative.

Best Practices

  1. Check compatibility before concatenating
# Verify shapes are compatible
print([adata.n_vars for adata in [adata1, adata2, adata3]])

# Check variable names match
print([set(adata.var_names) for adata in [adata1, adata2, adata3]])
  1. Use appropriate join type
  2. inner: When you need the same features across all samples (most stringent)
  3. outer: When you want to preserve all features (most inclusive)
  1. Track data sources

Always use label and keys to track which observations came from which dataset.

  1. Consider memory usage
  2. For large datasets, use AnnCollection or concat_on_disk
  3. Consider backed mode for the result
  1. Handle batch effects

Concatenation does not estimate batch effects. Choose an analysis method based on experimental design and matrix semantics; automatic correction can erase biology when batch and condition are confounded. See the scanpy skill for analysis.

  1. Validate results
# Check dimensions
print(adata_combined.shape)

# Check batch distribution
print(adata_combined.obs['batch'].value_counts())

# Verify metadata integrity
print(adata_combined.var.head())
print(adata_combined.obs.head())

pairwise=True creates block-diagonal graphs with no cross-batch neighbors; recompute a graph after integration when cross-batch relationships are needed. axis=1 aligns observations by name but does not make RNA counts and protein values statistically comparable; preserve modality and use a MuData container for separate matrices.

Sources: concat, AnnCollection, to_adata, concat_on_disk, and installed 0.13.4 source. Biological and user-file examples are illustrative.

Raw lines 372 to 421
Names a skill scanpy :397