Skill Tracer
Skills › Scientific Agent Skills › scanpy › standard_workflow.md

standard_workflow.md

238 lines · skills/scanpy/references/standard_workflow.md at 5f2bc03

Open on GitHub
1# Standard Scanpy Workflow for Single-Cell Analysis
2
3Reviewed for Scanpy 1.12.4 / AnnData 0.13.4 on 2026-10-01. These snippets are
4illustrative and require the named genes, groups and prior results to exist.
5Use the tested CLI workflow for a runnable starting point. Counts, normalized
6expression and `.raw` have distinct roles; see [upstream-review.md](upstream-review.md).
7
8
9This document outlines the standard workflow for analyzing single-cell RNA-seq data using scanpy.

Standard Scanpy Workflow for Single-Cell Analysis

Reviewed for Scanpy 1.12.4 / AnnData 0.13.4 on 2026-10-01. These snippets are illustrative and require the named genes, groups and prior results to exist. Use the tested CLI workflow for a runnable starting point. Counts, normalized expression and .raw have distinct roles; see upstream-review.md.

This document outlines the standard workflow for analyzing single-cell RNA-seq data using scanpy.

Raw lines 1 to 10
11## Complete Analysis Pipeline

Complete Analysis Pipeline

Raw lines 11 to 12
13### 1. Data Loading and Initial Setup
15```python
16import scanpy as sc
17import pandas as pd
18import numpy as np
20# Configure scanpy settings
21sc.settings.verbosity = 3 # verbosity: errors (0), warnings (1), info (2), hints (3)
22sc.set_figure_params(dpi=80, facecolor='white')
24# Load data (various formats)
25adata = sc.read_10x_mtx('path/to/data/') # For 10X data
26# adata = sc.read_h5ad('path/to/data.h5ad') # For h5ad format
27# adata = sc.read_csv('path/to/data.csv') # For CSV format
28```

1. Data Loading and Initial Setup

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

# Configure scanpy settings
sc.settings.verbosity = 3  # verbosity: errors (0), warnings (1), info (2), hints (3)
sc.set_figure_params(dpi=80, facecolor='white')

# Load data (various formats)
adata = sc.read_10x_mtx('path/to/data/')  # For 10X data
# adata = sc.read_h5ad('path/to/data.h5ad')  # For h5ad format
# adata = sc.read_csv('path/to/data.csv')  # For CSV format
Raw lines 13 to 29
30### 2. Quality Control (QC)
32```python
33# Human symbols shown; use organism/annotation-specific mitochondrial IDs
34adata.var["mt"] = adata.var_names.str.startswith("MT-")
35# Calculate QC metrics
36sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], percent_top=None, log1p=False, inplace=True)
38# Common filtering thresholds (adjust based on dataset)
39sc.pp.filter_cells(adata, min_genes=200)
40sc.pp.filter_genes(adata, min_cells=3)
42# Remove cells with high mitochondrial content
43adata = adata[adata.obs.pct_counts_mt < 5, :].copy()
45# Optional: doublet detection (run on raw counts before normalization)
46# sc.pp.scrublet(adata)
47# adata = adata[~adata.obs['predicted_doublet'], :].copy()
49# Visualize QC metrics
50sc.pl.violin(adata, ['n_genes_by_counts', 'total_counts', 'pct_counts_mt'],
51 jitter=0.4, multi_panel=True)
52sc.pl.scatter(adata, x='total_counts', y='pct_counts_mt')
53sc.pl.scatter(adata, x='total_counts', y='n_genes_by_counts')
54```

2. Quality Control (QC)

# Human symbols shown; use organism/annotation-specific mitochondrial IDs
adata.var["mt"] = adata.var_names.str.startswith("MT-")
# Calculate QC metrics
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], percent_top=None, log1p=False, inplace=True)

# Common filtering thresholds (adjust based on dataset)
sc.pp.filter_cells(adata, min_genes=200)
sc.pp.filter_genes(adata, min_cells=3)

# Remove cells with high mitochondrial content
adata = adata[adata.obs.pct_counts_mt < 5, :].copy()

# Optional: doublet detection (run on raw counts before normalization)
# sc.pp.scrublet(adata)
# adata = adata[~adata.obs['predicted_doublet'], :].copy()

# Visualize QC metrics
sc.pl.violin(adata, ['n_genes_by_counts', 'total_counts', 'pct_counts_mt'],
             jitter=0.4, multi_panel=True)
sc.pl.scatter(adata, x='total_counts', y='pct_counts_mt')
sc.pl.scatter(adata, x='total_counts', y='n_genes_by_counts')
Raw lines 30 to 55
56### 3. Normalization
58```python
59# Preserve identified raw counts BEFORE normalization
60adata.layers["counts"] = adata.X.copy()
61# Normalize to 10,000 counts per cell
62sc.pp.normalize_total(adata, target_sum=1e4)
64# Log-transform the data
65sc.pp.log1p(adata)
67# Store normalized data in raw for later use
68adata.raw = adata.copy()
69```

3. Normalization

# Preserve identified raw counts BEFORE normalization
adata.layers["counts"] = adata.X.copy()
# Normalize to 10,000 counts per cell
sc.pp.normalize_total(adata, target_sum=1e4)

# Log-transform the data
sc.pp.log1p(adata)

# Store normalized data in raw for later use
adata.raw = adata.copy()
Raw lines 56 to 70
71### 4. Feature Selection
73```python
74# Identify highly variable genes
75sc.pp.highly_variable_genes(adata, min_mean=0.0125, max_mean=3, min_disp=0.5)
77# Visualize highly variable genes
78sc.pl.highly_variable_genes(adata)
80# Gene subsetting also subsets counts: retain full-gene data for pseudobulk
81adata.write_h5ad("full_gene_lognorm.h5ad")
82# Subset to highly variable genes
83adata = adata[:, adata.var.highly_variable].copy()
84```

4. Feature Selection

# Identify highly variable genes
sc.pp.highly_variable_genes(adata, min_mean=0.0125, max_mean=3, min_disp=0.5)

# Visualize highly variable genes
sc.pl.highly_variable_genes(adata)

# Gene subsetting also subsets counts: retain full-gene data for pseudobulk
adata.write_h5ad("full_gene_lognorm.h5ad")
# Subset to highly variable genes
adata = adata[:, adata.var.highly_variable].copy()
Raw lines 71 to 85
86### 5. Scaling and Regression
88```python
89# Optional; may densify and remove true biological signal
90# sc.pp.regress_out(adata, ['total_counts', 'pct_counts_mt'])
92# Scale data to unit variance and zero mean
93sc.pp.scale(adata, max_value=10)
94```

5. Scaling and Regression

# Optional; may densify and remove true biological signal
# sc.pp.regress_out(adata, ['total_counts', 'pct_counts_mt'])

# Scale data to unit variance and zero mean
sc.pp.scale(adata, max_value=10)
Raw lines 86 to 95
96### 6. Dimensionality Reduction
98```python
99# Principal Component Analysis (PCA)
100n_pcs = min(40, adata.n_obs - 1, adata.n_vars - 1)
101sc.pp.pca(adata, n_comps=n_pcs, svd_solver='arpack')
103# Visualize PCA results
104sc.pl.pca(adata, color='CST3')
105sc.pl.pca_variance_ratio(adata, log=True)
107# Computing neighborhood graph
108sc.pp.neighbors(adata, n_neighbors=10, n_pcs=n_pcs, use_rep='X_pca')
110# UMAP for visualization
111sc.tl.umap(adata)
113# t-SNE (alternative to UMAP)
114# sc.tl.tsne(adata)
115```

6. Dimensionality Reduction

# Principal Component Analysis (PCA)
n_pcs = min(40, adata.n_obs - 1, adata.n_vars - 1)
sc.pp.pca(adata, n_comps=n_pcs, svd_solver='arpack')

# Visualize PCA results
sc.pl.pca(adata, color='CST3')
sc.pl.pca_variance_ratio(adata, log=True)

# Computing neighborhood graph
sc.pp.neighbors(adata, n_neighbors=10, n_pcs=n_pcs, use_rep='X_pca')

# UMAP for visualization
sc.tl.umap(adata)

# t-SNE (alternative to UMAP)
# sc.tl.tsne(adata)
Raw lines 96 to 116
117### 7. Clustering
119```python
120# Leiden clustering
121sc.tl.leiden(adata, resolution=0.5, flavor='igraph', directed=False, n_iterations=2)
123# Visualize clustering results
124sc.pl.umap(adata, color=['leiden'], legend_loc='on data')
125```

7. Clustering

# Leiden clustering
sc.tl.leiden(adata, resolution=0.5, flavor='igraph', directed=False, n_iterations=2)

# Visualize clustering results
sc.pl.umap(adata, color=['leiden'], legend_loc='on data')
Raw lines 117 to 126
127### 8. Marker Gene Identification
129`rank_genes_groups` is appropriate for exploratory cluster markers. Per-cell tests produce inflated significance; for rigorous DE between conditions, pseudobulk with `sc.get.aggregate()` and use pydeseq2.
131```python
132# Find marker genes for each cluster (exploratory)
133sc.tl.rank_genes_groups(adata, 'leiden', method='wilcoxon')
135# Visualize top marker genes
136sc.pl.rank_genes_groups(adata, n_genes=25, sharey=False)
138# Get marker gene dataframe
139marker_genes = sc.get.rank_genes_groups_df(adata, group='0')
141# Visualize specific markers
142sc.pl.umap(adata, color=['leiden', 'CST3', 'NKG7'])
143```

8. Marker Gene Identification

rank_genes_groups is appropriate for exploratory cluster markers. Per-cell tests produce inflated significance; for rigorous DE between conditions, pseudobulk with sc.get.aggregate() and use pydeseq2.

# Find marker genes for each cluster (exploratory)
sc.tl.rank_genes_groups(adata, 'leiden', method='wilcoxon')

# Visualize top marker genes
sc.pl.rank_genes_groups(adata, n_genes=25, sharey=False)

# Get marker gene dataframe
marker_genes = sc.get.rank_genes_groups_df(adata, group='0')

# Visualize specific markers
sc.pl.umap(adata, color=['leiden', 'CST3', 'NKG7'])
Raw lines 127 to 144
145### 9. Cell Type Annotation
147```python
148# Manual annotation based on marker genes
149cluster_annotations = {
150 '0': 'CD4 T cells',
151 '1': 'CD14+ Monocytes',
152 '2': 'B cells',
153 '3': 'CD8 T cells',
154 # ... add more annotations
156adata.obs['cell_type'] = adata.obs['leiden'].map(cluster_annotations)
158# Visualize annotated cell types
159sc.pl.umap(adata, color='cell_type', legend_loc='on data')
160```

9. Cell Type Annotation

# Manual annotation based on marker genes
cluster_annotations = {
    '0': 'CD4 T cells',
    '1': 'CD14+ Monocytes',
    '2': 'B cells',
    '3': 'CD8 T cells',
    # ... add more annotations
}
adata.obs['cell_type'] = adata.obs['leiden'].map(cluster_annotations)

# Visualize annotated cell types
sc.pl.umap(adata, color='cell_type', legend_loc='on data')
Raw lines 145 to 161
162### 10. Saving Results
164```python
165# Save the processed AnnData object
166adata.write('results/processed_data.h5ad')
168# Export results to CSV
169adata.obs.to_csv('results/cell_metadata.csv')
170adata.var.to_csv('results/gene_metadata.csv')
171```

10. Saving Results

# Save the processed AnnData object
adata.write('results/processed_data.h5ad')

# Export results to CSV
adata.obs.to_csv('results/cell_metadata.csv')
adata.var.to_csv('results/gene_metadata.csv')
Raw lines 162 to 172
173## Additional Analysis Options

Additional Analysis Options

Raw lines 173 to 174
175### Trajectory Inference
177```python
178# PAGA (Partition-based graph abstraction)
179sc.tl.paga(adata, groups='leiden')
180sc.pl.paga(adata, color=['leiden'])
182# Diffusion pseudotime (DPT)
183adata.uns['iroot'] = np.flatnonzero(adata.obs['leiden'] == '0')[0]
184sc.tl.diffmap(adata, n_comps=10) # choose fewer for small datasets
185sc.tl.dpt(adata, n_dcs=10)
186sc.pl.umap(adata, color=['dpt_pseudotime'])
187```

Trajectory Inference

# PAGA (Partition-based graph abstraction)
sc.tl.paga(adata, groups='leiden')
sc.pl.paga(adata, color=['leiden'])

# Diffusion pseudotime (DPT)
adata.uns['iroot'] = np.flatnonzero(adata.obs['leiden'] == '0')[0]
sc.tl.diffmap(adata, n_comps=10)  # choose fewer for small datasets
sc.tl.dpt(adata, n_dcs=10)
sc.pl.umap(adata, color=['dpt_pseudotime'])
Raw lines 175 to 188
189### Differential Expression Between Conditions
191Pseudobulk by sample and cell type, then run proper DE (e.g., pydeseq2):
193```python
194pb = sc.get.aggregate(
195 adata,
196 by=['sample', 'cell_type'],
197 func='sum',
198 layer='counts',
200# pb.layers["sum"] is samples x genes; pb.X is None.
201# Join condition/donor metadata, select one cell type and verify replication/design.
202```
204For quick exploratory comparisons only:
206```python
207sc.tl.rank_genes_groups(adata, groupby='condition', groups=['treated'],
208 reference='control', method='wilcoxon')
209sc.pl.rank_genes_groups(adata, groups=['treated'])
210```

Differential Expression Between Conditions

Pseudobulk by sample and cell type, then run proper DE (e.g., pydeseq2):

pb = sc.get.aggregate(
    adata,
    by=['sample', 'cell_type'],
    func='sum',
    layer='counts',
)
# pb.layers["sum"] is samples x genes; pb.X is None.
# Join condition/donor metadata, select one cell type and verify replication/design.

For quick exploratory comparisons only:

sc.tl.rank_genes_groups(adata, groupby='condition', groups=['treated'],
                         reference='control', method='wilcoxon')
sc.pl.rank_genes_groups(adata, groups=['treated'])
Raw lines 189 to 211
212### Gene Set Scoring
214```python
215# Score cells for gene set expression
216gene_set = ['CD3D', 'CD3E', 'CD3G']
217sc.tl.score_genes(adata, gene_set, score_name='T_cell_score')
218sc.pl.umap(adata, color='T_cell_score')
219```

Gene Set Scoring

# Score cells for gene set expression
gene_set = ['CD3D', 'CD3E', 'CD3G']
sc.tl.score_genes(adata, gene_set, score_name='T_cell_score')
sc.pl.umap(adata, color='T_cell_score')
Raw lines 212 to 220
221## Common Parameters to Adjust
223- **QC thresholds**: `min_genes`, `min_cells`, `pct_counts_mt` - depends on dataset quality
224- **Normalization target**: Usually 1e4, but can be adjusted
225- **HVG parameters**: Affects feature selection stringency
226- **PCA components**: Check variance ratio plot to determine optimal number
227- **Clustering resolution**: Higher values give more clusters (typically 0.4-1.2)
228- **n_neighbors**: Affects granularity of UMAP and clustering (typically 10-30)

Common Parameters to Adjust

  • QC thresholds: min_genes, min_cells, pct_counts_mt - depends on dataset quality
  • Normalization target: Usually 1e4, but can be adjusted
  • HVG parameters: Affects feature selection stringency
  • PCA components: Check variance ratio plot to determine optimal number
  • Clustering resolution: Higher values give more clusters (typically 0.4-1.2)
  • n_neighbors: Affects granularity of UMAP and clustering (typically 10-30)
Raw lines 221 to 229
230## Best Practices
2321. Always visualize QC metrics before filtering
2332. Save raw counts before normalization (`adata.layers["counts"] = adata.X.copy()`)
2343. Use Leiden clustering (`sc.tl.louvain` deprecated in scanpy 1.12)
2354. Try multiple clustering resolutions to find optimal granularity
2365. Validate cell type annotations with known marker genes
2376. Pseudobulk for rigorous DE; treat `rank_genes_groups` p-values as exploratory
2387. Save intermediate results at key steps

Best Practices

  1. Always visualize QC metrics before filtering
  2. Save raw counts before normalization (adata.layers["counts"] = adata.X.copy())
  3. Use Leiden clustering (sc.tl.louvain deprecated in scanpy 1.12)
  4. Try multiple clustering resolutions to find optimal granularity
  5. Validate cell type annotations with known marker genes
  6. Pseudobulk for rigorous DE; treat rank_genes_groups p-values as exploratory
  7. Save intermediate results at key steps
Raw lines 230 to 238