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

manipulation.md

546 lines · skills/anndata/references/manipulation.md at 5f2bc03

Open on GitHub

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

1# Data Manipulation
2
3Operations for transforming, subsetting, and manipulating AnnData objects.
4

Data Manipulation

Operations for transforming, subsetting, and manipulating AnnData objects.

Raw lines 1 to 4
5## Subsetting
6
7### By indices
8```python
9import anndata as ad
10import numpy as np
12adata = ad.AnnData(X=np.random.rand(1000, 2000))
14# Integer indices
15subset = adata[0:100, 0:500] # First 100 obs, first 500 vars
17# List of indices
18obs_indices = [0, 10, 20, 30, 40]
19var_indices = [0, 1, 2, 3, 4]
20subset = adata[obs_indices, var_indices]
22# Single observation or variable
23single_obs = adata[0, :]
24single_var = adata[:, 0]
25```

By indices

import anndata as ad
import numpy as np

adata = ad.AnnData(X=np.random.rand(1000, 2000))

# Integer indices
subset = adata[0:100, 0:500]  # First 100 obs, first 500 vars

# List of indices
obs_indices = [0, 10, 20, 30, 40]
var_indices = [0, 1, 2, 3, 4]
subset = adata[obs_indices, var_indices]

# Single observation or variable
single_obs = adata[0, :]
single_var = adata[:, 0]
Raw lines 7 to 26
27### By names
28```python
29import pandas as pd
31# Create with named indices
32obs_names = [f'cell_{i}' for i in range(1000)]
33var_names = [f'gene_{i}' for i in range(2000)]
34adata = ad.AnnData(
35 X=np.random.rand(1000, 2000),
36 obs=pd.DataFrame(index=obs_names),
37 var=pd.DataFrame(index=var_names)
40# Subset by observation names
41subset = adata[['cell_0', 'cell_1', 'cell_2'], :]
43# Subset by variable names
44subset = adata[:, ['gene_0', 'gene_10', 'gene_20']]
46# Both axes
47subset = adata[['cell_0', 'cell_1'], ['gene_0', 'gene_1']]
48```

By names

import pandas as pd

# Create with named indices
obs_names = [f'cell_{i}' for i in range(1000)]
var_names = [f'gene_{i}' for i in range(2000)]
adata = ad.AnnData(
    X=np.random.rand(1000, 2000),
    obs=pd.DataFrame(index=obs_names),
    var=pd.DataFrame(index=var_names)
)

# Subset by observation names
subset = adata[['cell_0', 'cell_1', 'cell_2'], :]

# Subset by variable names
subset = adata[:, ['gene_0', 'gene_10', 'gene_20']]

# Both axes
subset = adata[['cell_0', 'cell_1'], ['gene_0', 'gene_1']]
Raw lines 27 to 49
50### By boolean masks
51```python
52# Create boolean masks
53high_count_obs = np.random.rand(1000) > 0.5
54high_var_genes = np.random.rand(2000) > 0.7
56# Subset using masks
57subset = adata[high_count_obs, :]
58subset = adata[:, high_var_genes]
59subset = adata[high_count_obs, high_var_genes]
60```

By boolean masks

# Create boolean masks
high_count_obs = np.random.rand(1000) > 0.5
high_var_genes = np.random.rand(2000) > 0.7

# Subset using masks
subset = adata[high_count_obs, :]
subset = adata[:, high_var_genes]
subset = adata[high_count_obs, high_var_genes]
Raw lines 50 to 61
62### By metadata conditions
63```python
64# Add metadata
65adata.obs['cell_type'] = np.random.choice(['A', 'B', 'C'], 1000)
66adata.obs['quality_score'] = np.random.rand(1000)
67adata.var['highly_variable'] = np.random.rand(2000) > 0.8
69# Filter by cell type
70t_cells = adata[adata.obs['cell_type'] == 'A']
72# Filter by multiple conditions
73high_quality_a_cells = adata[
74 (adata.obs['cell_type'] == 'A') &
75 (adata.obs['quality_score'] > 0.7)
78# Filter by variable metadata
79hv_genes = adata[:, adata.var['highly_variable']]
81# Complex conditions
82filtered = adata[
83 (adata.obs['quality_score'] > 0.5) &
84 (adata.obs['cell_type'].isin(['A', 'B'])),
85 adata.var['highly_variable']
87```

By metadata conditions

# Add metadata
adata.obs['cell_type'] = np.random.choice(['A', 'B', 'C'], 1000)
adata.obs['quality_score'] = np.random.rand(1000)
adata.var['highly_variable'] = np.random.rand(2000) > 0.8

# Filter by cell type
t_cells = adata[adata.obs['cell_type'] == 'A']

# Filter by multiple conditions
high_quality_a_cells = adata[
    (adata.obs['cell_type'] == 'A') &
    (adata.obs['quality_score'] > 0.7)
]

# Filter by variable metadata
hv_genes = adata[:, adata.var['highly_variable']]

# Complex conditions
filtered = adata[
    (adata.obs['quality_score'] > 0.5) &
    (adata.obs['cell_type'].isin(['A', 'B'])),
    adata.var['highly_variable']
]
Raw lines 62 to 88
89## Transposition
91```python
92# Transpose AnnData object (swap observations and variables)
93adata_T = adata.copy().T
95# Shape changes
96print(adata.shape) # (1000, 2000)
97print(adata_T.shape) # (2000, 1000)
99# obs and var are swapped
100print(adata.obs.head()) # Observation metadata
101print(adata_T.var.head()) # Same data, now as variable metadata
103# Transposition drops .raw and is unsupported directly on views/backed arrays.
104# Materialize backed data with .to_memory() first if it fits.
105# Useful when data is in opposite orientation
106# Common with some file formats where genes are rows
107```

Transposition

# Transpose AnnData object (swap observations and variables)
adata_T = adata.copy().T

# Shape changes
print(adata.shape)    # (1000, 2000)
print(adata_T.shape)  # (2000, 1000)

# obs and var are swapped
print(adata.obs.head())   # Observation metadata
print(adata_T.var.head()) # Same data, now as variable metadata

# Transposition drops .raw and is unsupported directly on views/backed arrays.
# Materialize backed data with .to_memory() first if it fits.
# Useful when data is in opposite orientation
# Common with some file formats where genes are rows
Raw lines 89 to 108
109## Copying
111### Full copy
112```python
113# Create independent copy
114adata_copy = adata.copy()
116# Modifications to copy don't affect original
117adata_copy.obs['new_column'] = 1
118print('new_column' in adata.obs.columns) # False
119```

Full copy

# Create independent copy
adata_copy = adata.copy()

# Modifications to copy don't affect original
adata_copy.obs['new_column'] = 1
print('new_column' in adata.obs.columns)  # False
Raw lines 111 to 120
121### View
122```python
123# View (subset arrays are accessed lazily; writes can materialize a copy)
124adata_view = adata[0:100, :]
126# Check if object is a view
127print(adata_view.is_view) # True
129# Convert view to independent copy
130adata_independent = adata_view.copy()
131print(adata_independent.is_view) # False
132```

View

# View (subset arrays are accessed lazily; writes can materialize a copy)
adata_view = adata[0:100, :]

# Check if object is a view
print(adata_view.is_view)  # True

# Convert view to independent copy
adata_independent = adata_view.copy()
print(adata_independent.is_view)  # False
Raw lines 121 to 133
134## Renaming
136### Rename observations and variables
137```python
138# Rename all observations
139adata.obs_names = [f'new_cell_{i}' for i in range(adata.n_obs)]
141# Rename all variables
142adata.var_names = [f'new_gene_{i}' for i in range(adata.n_vars)]
144# Make names unique (add suffix to duplicates)
145adata.obs_names_make_unique()
146adata.var_names_make_unique()
147```

Rename observations and variables

# Rename all observations
adata.obs_names = [f'new_cell_{i}' for i in range(adata.n_obs)]

# Rename all variables
adata.var_names = [f'new_gene_{i}' for i in range(adata.n_vars)]

# Make names unique (add suffix to duplicates)
adata.obs_names_make_unique()
adata.var_names_make_unique()
Raw lines 136 to 148
149### Rename categories
150```python
151# Create categorical column
152adata.obs['cell_type'] = pd.Categorical(['A', 'B', 'C'] * 333 + ['A'])
154# Rename categories
155adata.rename_categories('cell_type', ['Type_A', 'Type_B', 'Type_C'])
157# Mapping input is unsupported by AnnData.rename_categories; preserve category order.
158rename = {'Type_A': 'T_cell', 'Type_B': 'B_cell', 'Type_C': 'Monocyte'}
159adata.rename_categories(
160 'cell_type', [rename.get(c, c) for c in adata.obs['cell_type'].cat.categories]
162```

Rename categories

# Create categorical column
adata.obs['cell_type'] = pd.Categorical(['A', 'B', 'C'] * 333 + ['A'])

# Rename categories
adata.rename_categories('cell_type', ['Type_A', 'Type_B', 'Type_C'])

# Mapping input is unsupported by AnnData.rename_categories; preserve category order.
rename = {'Type_A': 'T_cell', 'Type_B': 'B_cell', 'Type_C': 'Monocyte'}
adata.rename_categories(
    'cell_type', [rename.get(c, c) for c in adata.obs['cell_type'].cat.categories]
)
Raw lines 149 to 163
164## Type Conversions

Type Conversions

Raw lines 164 to 165
166### Strings to categoricals
167```python
168# Convert string columns to categorical (more memory efficient)
169adata.obs['cell_type'] = ['TypeA', 'TypeB'] * 500
170adata.obs['tissue'] = ['brain', 'liver'] * 500
172# Convert all string columns to categorical
173adata.strings_to_categoricals()
175print(adata.obs['cell_type'].dtype) # category
176print(adata.obs['tissue'].dtype) # category
177```

Strings to categoricals

# Convert string columns to categorical (more memory efficient)
adata.obs['cell_type'] = ['TypeA', 'TypeB'] * 500
adata.obs['tissue'] = ['brain', 'liver'] * 500

# Convert all string columns to categorical
adata.strings_to_categoricals()

print(adata.obs['cell_type'].dtype)  # category
print(adata.obs['tissue'].dtype)     # category
Raw lines 166 to 178
179### Sparse to dense and vice versa
180```python
181from scipy.sparse import csr_matrix, issparse
183# Dense to sparse
184if not issparse(adata.X):
185 adata.X = csr_matrix(adata.X)
187# Sparse to dense
188if issparse(adata.X):
189 adata.X = adata.X.toarray()
191# Convert layer
192adata.layers['normalized'] = csr_matrix(adata.layers['normalized'])
193```

Sparse to dense and vice versa

from scipy.sparse import csr_matrix, issparse

# Dense to sparse
if not issparse(adata.X):
    adata.X = csr_matrix(adata.X)

# Sparse to dense
if issparse(adata.X):
    adata.X = adata.X.toarray()

# Convert layer
adata.layers['normalized'] = csr_matrix(adata.layers['normalized'])
Raw lines 179 to 194
195## Chunked Operations
197Process large datasets in chunks:
199```python
200# Iterate through data in chunks
201chunk_size = 100
202for chunk, start, stop in adata.chunked_X(chunk_size):
203 # Process chunk
204 result = process_chunk(chunk)
205```

Chunked Operations

Process large datasets in chunks:

# Iterate through data in chunks
chunk_size = 100
for chunk, start, stop in adata.chunked_X(chunk_size):
    # Process chunk
    result = process_chunk(chunk)
Raw lines 195 to 206
207## Extracting Vectors

Extracting Vectors

Raw lines 207 to 208
209### Get observation vectors
210```python
211# Get observation metadata as array
212cell_types = adata.obs_vector('cell_type')
214# Get gene expression across observations
215actb_expression = adata.obs_vector('ACTB') # If ACTB in var_names
216```

Get observation vectors

# Get observation metadata as array
cell_types = adata.obs_vector('cell_type')

# Get gene expression across observations
actb_expression = adata.obs_vector('ACTB')  # If ACTB in var_names
Raw lines 209 to 217
218### Get variable vectors
219```python
220# Get variable metadata as array
221gene_names = adata.var_vector('gene_name')
222```

Get variable vectors

# Get variable metadata as array
gene_names = adata.var_vector('gene_name')
Raw lines 218 to 223
224## Adding/Modifying Data

Adding/Modifying Data

Raw lines 224 to 225
226### Add observations
227```python
228# Create new observations
229new_obs = ad.AnnData(X=np.random.rand(100, adata.n_vars))
230new_obs.var_names = adata.var_names
232# Concatenate with existing
233adata_extended = ad.concat([adata, new_obs], axis=0)
234```

Add observations

# Create new observations
new_obs = ad.AnnData(X=np.random.rand(100, adata.n_vars))
new_obs.var_names = adata.var_names

# Concatenate with existing
adata_extended = ad.concat([adata, new_obs], axis=0)
Raw lines 226 to 235
236### Add variables
237```python
238# Create new variables
239new_vars = ad.AnnData(X=np.random.rand(adata.n_obs, 100))
240new_vars.obs_names = adata.obs_names
242# Concatenate with existing
243adata_extended = ad.concat([adata, new_vars], axis=1)
244```

Add variables

# Create new variables
new_vars = ad.AnnData(X=np.random.rand(adata.n_obs, 100))
new_vars.obs_names = adata.obs_names

# Concatenate with existing
adata_extended = ad.concat([adata, new_vars], axis=1)
Raw lines 236 to 245
246### Add metadata columns
247```python
248# Add observation annotation
249adata.obs['new_score'] = np.random.rand(adata.n_obs)
251# Add variable annotation
252adata.var['new_label'] = ['label'] * adata.n_vars
254# Add from external data
255external_data = pd.read_csv('metadata.csv', index_col=0)
256adata.obs['external_info'] = external_data.loc[adata.obs_names, 'column']
257```

Add metadata columns

# Add observation annotation
adata.obs['new_score'] = np.random.rand(adata.n_obs)

# Add variable annotation
adata.var['new_label'] = ['label'] * adata.n_vars

# Add from external data
external_data = pd.read_csv('metadata.csv', index_col=0)
adata.obs['external_info'] = external_data.loc[adata.obs_names, 'column']
Raw lines 246 to 258
259### Add layers
260```python
261# Add new layer
262adata.layers['raw_counts'] = np.random.randint(0, 100, adata.shape)
263adata.layers['log_transformed'] = np.log1p(adata.X)
265# Replace layer
266adata.layers['normalized'] = new_normalized_data
267```

Add layers

# Add new layer
adata.layers['raw_counts'] = np.random.randint(0, 100, adata.shape)
adata.layers['log_transformed'] = np.log1p(adata.X)

# Replace layer
adata.layers['normalized'] = new_normalized_data
Raw lines 259 to 268
269### Add embeddings
270```python
271# Add PCA
272adata.obsm['X_pca'] = np.random.rand(adata.n_obs, 50)
274# Add UMAP
275adata.obsm['X_umap'] = np.random.rand(adata.n_obs, 2)
277# Add multiple embeddings
278adata.obsm['X_tsne'] = np.random.rand(adata.n_obs, 2)
279adata.obsm['X_diffmap'] = np.random.rand(adata.n_obs, 10)
280```

Add embeddings

# Add PCA
adata.obsm['X_pca'] = np.random.rand(adata.n_obs, 50)

# Add UMAP
adata.obsm['X_umap'] = np.random.rand(adata.n_obs, 2)

# Add multiple embeddings
adata.obsm['X_tsne'] = np.random.rand(adata.n_obs, 2)
adata.obsm['X_diffmap'] = np.random.rand(adata.n_obs, 10)
Raw lines 269 to 281
282### Add pairwise relationships
283```python
284from scipy.sparse import csr_matrix
286# Add nearest neighbor graph
287n_obs = adata.n_obs
288knn_graph = csr_matrix(np.random.rand(n_obs, n_obs) > 0.95)
289adata.obsp['connectivities'] = knn_graph
291# Add distance matrix
292adata.obsp['distances'] = csr_matrix(np.random.rand(n_obs, n_obs))
293```

Add pairwise relationships

from scipy.sparse import csr_matrix

# Add nearest neighbor graph
n_obs = adata.n_obs
knn_graph = csr_matrix(np.random.rand(n_obs, n_obs) > 0.95)
adata.obsp['connectivities'] = knn_graph

# Add distance matrix
adata.obsp['distances'] = csr_matrix(np.random.rand(n_obs, n_obs))
Raw lines 282 to 294
295### Add unstructured data
296```python
297# Add analysis parameters
298adata.uns['pca'] = {
299 'variance': [0.2, 0.15, 0.1],
300 'variance_ratio': [0.4, 0.3, 0.2],
301 'params': {'n_comps': 50}
304# Add color schemes
305adata.uns['cell_type_colors'] = ['#FF0000', '#00FF00', '#0000FF']
306```

Add unstructured data

# Add analysis parameters
adata.uns['pca'] = {
    'variance': [0.2, 0.15, 0.1],
    'variance_ratio': [0.4, 0.3, 0.2],
    'params': {'n_comps': 50}
}

# Add color schemes
adata.uns['cell_type_colors'] = ['#FF0000', '#00FF00', '#0000FF']
Raw lines 295 to 307
308## Removing Data
310### Remove observations or variables
311```python
312# Keep only specific observations
313keep_obs = adata.obs['quality_score'] > 0.5
314adata = adata[keep_obs, :]
316# Remove specific variables
317remove_vars = adata.var['low_count']
318adata = adata[:, ~remove_vars]
319```

Remove observations or variables

# Keep only specific observations
keep_obs = adata.obs['quality_score'] > 0.5
adata = adata[keep_obs, :]

# Remove specific variables
remove_vars = adata.var['low_count']
adata = adata[:, ~remove_vars]
Raw lines 310 to 320
321### Remove metadata columns
322```python
323# Remove observation column
324adata.obs.drop('unwanted_column', axis=1, inplace=True)
326# Remove variable column
327adata.var.drop('unwanted_column', axis=1, inplace=True)
328```

Remove metadata columns

# Remove observation column
adata.obs.drop('unwanted_column', axis=1, inplace=True)

# Remove variable column
adata.var.drop('unwanted_column', axis=1, inplace=True)
Raw lines 321 to 329
330### Remove layers
331```python
332# Remove specific layer
333del adata.layers['unwanted_layer']
335# Remove named layers while explicitly preserving X (layers[None])
336adata.layers.clear(keep_x=True)
337```

Remove layers

# Remove specific layer
del adata.layers['unwanted_layer']

# Remove named layers while explicitly preserving X (layers[None])
adata.layers.clear(keep_x=True)
Raw lines 330 to 338
339### Remove embeddings
340```python
341# Remove specific embedding
342del adata.obsm['X_tsne']
344# Remove all embeddings
345adata.obsm = {}
346```

Remove embeddings

# Remove specific embedding
del adata.obsm['X_tsne']

# Remove all embeddings
adata.obsm = {}
Raw lines 339 to 347
348### Remove unstructured data
349```python
350# Remove specific key
351del adata.uns['unwanted_key']
353# Remove all unstructured data
354adata.uns = {}
355```

Remove unstructured data

# Remove specific key
del adata.uns['unwanted_key']

# Remove all unstructured data
adata.uns = {}
Raw lines 348 to 356
357## Reordering
359### Sort observations
360```python
361# Sort by observation metadata
362adata = adata[adata.obs.sort_values('quality_score').index, :]
364# Sort by observation names
365adata = adata[sorted(adata.obs_names), :]
366```

Sort observations

# Sort by observation metadata
adata = adata[adata.obs.sort_values('quality_score').index, :]

# Sort by observation names
adata = adata[sorted(adata.obs_names), :]
Raw lines 359 to 367
368### Sort variables
369```python
370# Sort by variable metadata
371adata = adata[:, adata.var.sort_values('gene_name').index]
373# Sort by variable names
374adata = adata[:, sorted(adata.var_names)]
375```

Sort variables

# Sort by variable metadata
adata = adata[:, adata.var.sort_values('gene_name').index]

# Sort by variable names
adata = adata[:, sorted(adata.var_names)]
Raw lines 368 to 376
377### Reorder to match external list
378```python
379# Reorder observations to match external list
380desired_order = ['cell_10', 'cell_5', 'cell_20']
381adata = adata[desired_order, :]
383# Reorder variables
384desired_genes = ['TP53', 'ACTB', 'GAPDH']
385adata = adata[:, desired_genes]
386```

Reorder to match external list

# Reorder observations to match external list
desired_order = ['cell_10', 'cell_5', 'cell_20']
adata = adata[desired_order, :]

# Reorder variables
desired_genes = ['TP53', 'ACTB', 'GAPDH']
adata = adata[:, desired_genes]
Raw lines 377 to 387
388## Data Transformations

Data Transformations

Raw lines 388 to 389
390### Normalize
392For nonnegative count matrices, library-size normalization preserves sparsity.
393This is not TPM (which also needs feature lengths). Keep original counts and
394record the target sum; cells with zero counts remain zero.
396```python
397from scipy.sparse import issparse
399counts = adata.X.copy()
400total_counts = np.asarray(counts.sum(axis=1)).ravel()
401scale = np.divide(1e4, total_counts, out=np.zeros_like(total_counts, dtype=float),
402 where=total_counts > 0)
403adata.layers['counts'] = counts
404normalized = counts.multiply(scale[:, None]).tocsr() if issparse(counts) else counts * scale[:, None]
405adata.layers['normalized'] = normalized
406logged = normalized.copy()
407if issparse(logged):
408 logged.data = np.log1p(logged.data) # log1p(0)=0, so implicit zeros stay correct
409else:
410 logged = np.log1p(logged)
411adata.layers['log1p'] = logged
412adata.uns['normalization'] = {'method': 'library_size_then_log1p', 'target_sum': 1e4}
413```
415Centering destroys sparsity. For a small dense matrix only, protect constant columns:
417```python
418assert isinstance(adata.X, np.ndarray)
419mean = adata.X.mean(axis=0)
420std = adata.X.std(axis=0)
421adata.layers['scaled'] = (adata.X - mean) / np.where(std == 0, 1, std)
422```

Normalize

For nonnegative count matrices, library-size normalization preserves sparsity. This is not TPM (which also needs feature lengths). Keep original counts and record the target sum; cells with zero counts remain zero.

from scipy.sparse import issparse

counts = adata.X.copy()
total_counts = np.asarray(counts.sum(axis=1)).ravel()
scale = np.divide(1e4, total_counts, out=np.zeros_like(total_counts, dtype=float),
                  where=total_counts > 0)
adata.layers['counts'] = counts
normalized = counts.multiply(scale[:, None]).tocsr() if issparse(counts) else counts * scale[:, None]
adata.layers['normalized'] = normalized
logged = normalized.copy()
if issparse(logged):
    logged.data = np.log1p(logged.data)  # log1p(0)=0, so implicit zeros stay correct
else:
    logged = np.log1p(logged)
adata.layers['log1p'] = logged
adata.uns['normalization'] = {'method': 'library_size_then_log1p', 'target_sum': 1e4}

Centering destroys sparsity. For a small dense matrix only, protect constant columns:

assert isinstance(adata.X, np.ndarray)
mean = adata.X.mean(axis=0)
std = adata.X.std(axis=0)
adata.layers['scaled'] = (adata.X - mean) / np.where(std == 0, 1, std)
Raw lines 390 to 423
424### Filter
425```python
426# Filter cells by total counts
427total_counts = np.array(adata.X.sum(axis=1)).flatten()
428adata.obs['total_counts'] = total_counts
429adata = adata[adata.obs['total_counts'] > 1000, :]
431# Filter genes by detection rate
432detection_rate = (adata.X > 0).sum(axis=0) / adata.n_obs
433adata.var['detection_rate'] = np.array(detection_rate).flatten()
434adata = adata[:, adata.var['detection_rate'] > 0.01]
435```

Filter

# Filter cells by total counts
total_counts = np.array(adata.X.sum(axis=1)).flatten()
adata.obs['total_counts'] = total_counts
adata = adata[adata.obs['total_counts'] > 1000, :]

# Filter genes by detection rate
detection_rate = (adata.X > 0).sum(axis=0) / adata.n_obs
adata.var['detection_rate'] = np.array(detection_rate).flatten()
adata = adata[:, adata.var['detection_rate'] > 0.01]
Raw lines 424 to 436
437## Working with Views
439Views are lightweight references to subsets of data that don't copy the underlying matrix:
441```python
442# Create view
443view = adata[0:100, 0:500]
444print(view.is_view) # True
446# Views allow read access
447data = view.X
449# In 0.13, .X and annotation writes use copy-on-write; they may allocate.
450# Copy explicitly before editing for clear ownership across package versions.
452# Convert view to independent copy
453independent = view.copy()
455# Force AnnData to be a copy, not a view
456adata = adata.copy()
457```

Working with Views

Views are lightweight references to subsets of data that don't copy the underlying matrix:

# Create view
view = adata[0:100, 0:500]
print(view.is_view)  # True

# Views allow read access
data = view.X

# In 0.13, .X and annotation writes use copy-on-write; they may allocate.
# Copy explicitly before editing for clear ownership across package versions.

# Convert view to independent copy
independent = view.copy()

# Force AnnData to be a copy, not a view
adata = adata.copy()
Raw lines 437 to 458
459## Merging Metadata
461```python
462# Merge external metadata
463external_metadata = pd.read_csv('additional_metadata.csv', index_col=0)
465# One row per identifier; retain observation order and length.
466assert external_metadata.index.is_unique
467assert adata.obs_names.isin(external_metadata.index).all()
468adata.obs = adata.obs.join(external_metadata, how='left', validate='one_to_one')
470# Left join (keep all adata observations)
471adata.obs = adata.obs.merge(
472 external_metadata,
473 left_index=True,
474 right_index=True,
475 how='left',
476 validate='one_to_one'
477).reindex(adata.obs_names)
478```

Merging Metadata

# Merge external metadata
external_metadata = pd.read_csv('additional_metadata.csv', index_col=0)

# One row per identifier; retain observation order and length.
assert external_metadata.index.is_unique
assert adata.obs_names.isin(external_metadata.index).all()
adata.obs = adata.obs.join(external_metadata, how='left', validate='one_to_one')

# Left join (keep all adata observations)
adata.obs = adata.obs.merge(
    external_metadata,
    left_index=True,
    right_index=True,
    how='left',
    validate='one_to_one'
).reindex(adata.obs_names)
Raw lines 459 to 479
480## Common Manipulation Patterns

Common Manipulation Patterns

Raw lines 480 to 481
482### Quality control filtering
483```python
484# Calculate QC metrics
485adata.obs['n_genes'] = np.asarray((adata.X > 0).sum(axis=1)).ravel()
486adata.obs['total_counts'] = np.asarray(adata.X.sum(axis=1)).ravel()
487adata.var['n_cells'] = np.asarray((adata.X > 0).sum(axis=0)).ravel()
489# Filter low-quality cells
490adata = adata[adata.obs['n_genes'] > 200, :]
491adata = adata[adata.obs['total_counts'] < 50000, :]
493# Filter rarely detected genes
494adata = adata[:, adata.var['n_cells'] >= 3]
495```

Quality control filtering

# Calculate QC metrics
adata.obs['n_genes'] = np.asarray((adata.X > 0).sum(axis=1)).ravel()
adata.obs['total_counts'] = np.asarray(adata.X.sum(axis=1)).ravel()
adata.var['n_cells'] = np.asarray((adata.X > 0).sum(axis=0)).ravel()

# Filter low-quality cells
adata = adata[adata.obs['n_genes'] > 200, :]
adata = adata[adata.obs['total_counts'] < 50000, :]

# Filter rarely detected genes
adata = adata[:, adata.var['n_cells'] >= 3]
Raw lines 482 to 496
497### Select highly variable genes
498```python
499# Illustrative variance filter for a small DENSE matrix, not a biological HVG method.
500# Use the scanpy skill for batch-aware/mean-adjusted HVG selection.
501assert isinstance(adata.X, np.ndarray)
502gene_variance = np.var(adata.X, axis=0)
503adata.var['variance'] = np.array(gene_variance).flatten()
504adata.var['highly_variable'] = adata.var['variance'] > np.percentile(gene_variance, 90)
506# Subset to highly variable genes
507adata_hvg = adata[:, adata.var['highly_variable']].copy()
508```

Select highly variable genes

# Illustrative variance filter for a small DENSE matrix, not a biological HVG method.
# Use the scanpy skill for batch-aware/mean-adjusted HVG selection.
assert isinstance(adata.X, np.ndarray)
gene_variance = np.var(adata.X, axis=0)
adata.var['variance'] = np.array(gene_variance).flatten()
adata.var['highly_variable'] = adata.var['variance'] > np.percentile(gene_variance, 90)

# Subset to highly variable genes
adata_hvg = adata[:, adata.var['highly_variable']].copy()
Raw lines 497 to 509
Names a skill scanpy :500
510### Downsample
511```python
512# Random sampling of observations
513np.random.seed(42)
514n_sample = min(500, adata.n_obs)
515sample_indices = np.random.choice(adata.n_obs, n_sample, replace=False)
516adata_downsampled = adata[sample_indices, :].copy()
518# Stratified sampling by cell type
519from sklearn.model_selection import train_test_split
520train_idx, test_idx = train_test_split(
521 range(adata.n_obs),
522 test_size=0.2,
523 stratify=adata.obs['cell_type'],
524 random_state=42
526adata_train = adata[train_idx, :].copy()
527adata_test = adata[test_idx, :].copy()
528```

Downsample

# Random sampling of observations
np.random.seed(42)
n_sample = min(500, adata.n_obs)
sample_indices = np.random.choice(adata.n_obs, n_sample, replace=False)
adata_downsampled = adata[sample_indices, :].copy()

# Stratified sampling by cell type
from sklearn.model_selection import train_test_split
train_idx, test_idx = train_test_split(
    range(adata.n_obs),
    test_size=0.2,
    stratify=adata.obs['cell_type'],
    random_state=42
)
adata_train = adata[train_idx, :].copy()
adata_test = adata[test_idx, :].copy()
Raw lines 510 to 529
530### Split train/test
531```python
532# Random train/test split
533np.random.seed(42)
534n_obs = adata.n_obs
535train_size = int(0.8 * n_obs)
536indices = np.random.permutation(n_obs)
537train_indices = indices[:train_size]
538test_indices = indices[train_size:]
540adata_train = adata[train_indices, :].copy()
541adata_test = adata[test_indices, :].copy()
542```
544Random cell splits are illustrative only: use donor/sample-level grouping for held-out biological evaluation to prevent leakage. Fit preprocessing on training data and then transform held-out data.
546Reviewed against [AnnData methods](https://anndata.readthedocs.io/en/stable/generated/anndata.AnnData.html), [rename_categories](https://anndata.readthedocs.io/en/stable/generated/anndata.AnnData.rename_categories.html), [chunked_X](https://anndata.readthedocs.io/en/stable/generated/anndata.AnnData.chunked_X.html), and the [0.13 copy-on-write release change](https://anndata.readthedocs.io/en/stable/release-notes/), plus installed 0.13.4 source. User-file and analysis-function placeholders are illustrative.

Split train/test

# Random train/test split
np.random.seed(42)
n_obs = adata.n_obs
train_size = int(0.8 * n_obs)
indices = np.random.permutation(n_obs)
train_indices = indices[:train_size]
test_indices = indices[train_size:]

adata_train = adata[train_indices, :].copy()
adata_test = adata[test_indices, :].copy()

Random cell splits are illustrative only: use donor/sample-level grouping for held-out biological evaluation to prevent leakage. Fit preprocessing on training data and then transform held-out data.

Reviewed against AnnData methods, rename_categories, chunked_X, and the 0.13 copy-on-write release change, plus installed 0.13.4 source. User-file and analysis-function placeholders are illustrative.

Raw lines 530 to 546