Skill Tracer
Skills › Knowledge Work Plugins › scvi-tools › atac_peakvi.md

atac_peakvi.md

398 lines · bio-research/skills/scvi-tools/references/atac_peakvi.md at 2574e29

Open on GitHub
1# scATAC-seq Analysis with PeakVI
2
3This reference covers single-cell ATAC-seq analysis using PeakVI for dimensionality reduction, batch correction, and differential accessibility.
4

scATAC-seq Analysis with PeakVI

This reference covers single-cell ATAC-seq analysis using PeakVI for dimensionality reduction, batch correction, and differential accessibility.

Raw lines 1 to 4
5## Overview
6
7PeakVI is a deep generative model for scATAC-seq data that:
8- Models binary accessibility (peak open/closed)
9- Handles batch effects
10- Provides latent representation for clustering
11- Enables differential accessibility analysis

Overview

PeakVI is a deep generative model for scATAC-seq data that:

  • Models binary accessibility (peak open/closed)
  • Handles batch effects
  • Provides latent representation for clustering
  • Enables differential accessibility analysis
Raw lines 5 to 12
13## Prerequisites
15```python
16import scvi
17import scanpy as sc
18import numpy as np
19import anndata as ad
21print(f"scvi-tools version: {scvi.__version__}")
22```

Prerequisites

import scvi
import scanpy as sc
import numpy as np
import anndata as ad

print(f"scvi-tools version: {scvi.__version__}")
Raw lines 13 to 23
24## Step 1: Load and Prepare ATAC Data

Step 1: Load and Prepare ATAC Data

Raw lines 24 to 25
26### From 10x Genomics (Cell Ranger ATAC)
28```python
29# Peak-cell matrix from fragments
30# Usually in filtered_peak_bc_matrix format
32adata = sc.read_10x_h5("filtered_peak_bc_matrix.h5")
34# Or from mtx format
35adata = sc.read_10x_mtx("filtered_peak_bc_matrix/")
37# Check structure
38print(f"Cells: {adata.n_obs}, Peaks: {adata.n_vars}")
39print(f"Sparsity: {1 - adata.X.nnz / (adata.n_obs * adata.n_vars):.2%}")
40```

From 10x Genomics (Cell Ranger ATAC)

# Peak-cell matrix from fragments
# Usually in filtered_peak_bc_matrix format

adata = sc.read_10x_h5("filtered_peak_bc_matrix.h5")

# Or from mtx format
adata = sc.read_10x_mtx("filtered_peak_bc_matrix/")

# Check structure
print(f"Cells: {adata.n_obs}, Peaks: {adata.n_vars}")
print(f"Sparsity: {1 - adata.X.nnz / (adata.n_obs * adata.n_vars):.2%}")
Raw lines 26 to 41
42### From ArchR/Signac
44```python
45# Export from ArchR (in R)
46# saveArchRProject(proj, outputDirectory="atac_export", load=FALSE)
47# Then read the exported files in Python
49# From Signac:
50# Export peak matrix and metadata
51```

From ArchR/Signac

# Export from ArchR (in R)
# saveArchRProject(proj, outputDirectory="atac_export", load=FALSE)
# Then read the exported files in Python

# From Signac:
# Export peak matrix and metadata
Raw lines 42 to 52
53## Step 2: Quality Control
55```python
56# Calculate QC metrics
57sc.pp.calculate_qc_metrics(adata, inplace=True)
59# Key metrics for ATAC:
60# - n_genes_by_counts: peaks per cell (should rename)
61# - total_counts: fragments per cell
62adata.obs['n_peaks'] = adata.obs['n_genes_by_counts']
63adata.obs['total_fragments'] = adata.obs['total_counts']
65# Filter cells
66adata = adata[adata.obs['n_peaks'] > 500].copy()
67adata = adata[adata.obs['n_peaks'] < 50000].copy() # Remove potential doublets
69# Filter peaks (accessible in at least n cells)
70sc.pp.filter_genes(adata, min_cells=10)
72print(f"After QC: {adata.shape}")
73```

Step 2: Quality Control

# Calculate QC metrics
sc.pp.calculate_qc_metrics(adata, inplace=True)

# Key metrics for ATAC:
# - n_genes_by_counts: peaks per cell (should rename)
# - total_counts: fragments per cell
adata.obs['n_peaks'] = adata.obs['n_genes_by_counts']
adata.obs['total_fragments'] = adata.obs['total_counts']

# Filter cells
adata = adata[adata.obs['n_peaks'] > 500].copy()
adata = adata[adata.obs['n_peaks'] < 50000].copy()  # Remove potential doublets

# Filter peaks (accessible in at least n cells)
sc.pp.filter_genes(adata, min_cells=10)

print(f"After QC: {adata.shape}")
Raw lines 53 to 74
75### Binarize Data
77```python
78# PeakVI works with binary accessibility
79# Binarize if not already binary
80adata.X = (adata.X > 0).astype(np.float32)
82# Verify
83print(f"Unique values: {np.unique(adata.X.data)}")
84```

Binarize Data

# PeakVI works with binary accessibility
# Binarize if not already binary
adata.X = (adata.X > 0).astype(np.float32)

# Verify
print(f"Unique values: {np.unique(adata.X.data)}")
Raw lines 75 to 85
86## Step 3: Feature Selection
88Unlike RNA-seq, peak selection for ATAC is less established. Options:

Step 3: Feature Selection

Unlike RNA-seq, peak selection for ATAC is less established. Options:

Raw lines 86 to 89
90### Option A: Most Accessible Peaks
92```python
93# Select top peaks by accessibility frequency
94peak_accessibility = np.array(adata.X.sum(axis=0)).flatten()
95top_peaks = np.argsort(peak_accessibility)[-50000:] # Top 50k peaks
97adata = adata[:, top_peaks].copy()
98```

Option A: Most Accessible Peaks

# Select top peaks by accessibility frequency
peak_accessibility = np.array(adata.X.sum(axis=0)).flatten()
top_peaks = np.argsort(peak_accessibility)[-50000:]  # Top 50k peaks

adata = adata[:, top_peaks].copy()
Raw lines 90 to 99
100### Option B: Variable Peaks
102```python
103# Select peaks with high variance
104# (Most informative for clustering)
105from sklearn.feature_selection import VarianceThreshold
107selector = VarianceThreshold(threshold=0.05)
108selector.fit(adata.X)
109adata = adata[:, selector.get_support()].copy()
110```

Option B: Variable Peaks

# Select peaks with high variance
# (Most informative for clustering)
from sklearn.feature_selection import VarianceThreshold

selector = VarianceThreshold(threshold=0.05)
selector.fit(adata.X)
adata = adata[:, selector.get_support()].copy()
Raw lines 100 to 111
112### Option C: Peaks Near Genes
114```python
115# Keep peaks within promoter regions or gene bodies
116# Requires peak annotation
117# gene_peaks = peaks with gene annotation
118# adata = adata[:, adata.var['near_gene']].copy()
119```

Option C: Peaks Near Genes

# Keep peaks within promoter regions or gene bodies
# Requires peak annotation
# gene_peaks = peaks with gene annotation
# adata = adata[:, adata.var['near_gene']].copy()
Raw lines 112 to 120
121## Step 4: Add Batch Information
123```python
124# Add batch annotation if multiple samples
125adata.obs['batch'] = adata.obs['sample_id'] # Or appropriate column
127print(adata.obs['batch'].value_counts())
128```

Step 4: Add Batch Information

# Add batch annotation if multiple samples
adata.obs['batch'] = adata.obs['sample_id']  # Or appropriate column

print(adata.obs['batch'].value_counts())
Raw lines 121 to 129
130## Step 5: Setup and Train PeakVI
132```python
133# Setup AnnData
134scvi.model.PEAKVI.setup_anndata(
135 adata,
136 batch_key="batch" # Optional, omit for single batch
139# Create model
140model = scvi.model.PEAKVI(
141 adata,
142 n_latent=20, # Latent dimensions
143 n_layers_encoder=2,
144 n_layers_decoder=2
147# Train
148model.train(
149 max_epochs=200,
150 early_stopping=True,
151 batch_size=128
154# Check training
155model.history['elbo_train'].plot()
156```

Step 5: Setup and Train PeakVI

# Setup AnnData
scvi.model.PEAKVI.setup_anndata(
    adata,
    batch_key="batch"  # Optional, omit for single batch
)

# Create model
model = scvi.model.PEAKVI(
    adata,
    n_latent=20,      # Latent dimensions
    n_layers_encoder=2,
    n_layers_decoder=2
)

# Train
model.train(
    max_epochs=200,
    early_stopping=True,
    batch_size=128
)

# Check training
model.history['elbo_train'].plot()
Raw lines 130 to 157
158## Step 6: Get Latent Representation
160```python
161# Latent space for downstream analysis
162adata.obsm["X_PeakVI"] = model.get_latent_representation()
164# Clustering and visualization
165sc.pp.neighbors(adata, use_rep="X_PeakVI", n_neighbors=15)
166sc.tl.umap(adata)
167sc.tl.leiden(adata, resolution=0.5)
169# Visualize
170sc.pl.umap(adata, color=['leiden', 'batch'], ncols=2)
171```

Step 6: Get Latent Representation

# Latent space for downstream analysis
adata.obsm["X_PeakVI"] = model.get_latent_representation()

# Clustering and visualization
sc.pp.neighbors(adata, use_rep="X_PeakVI", n_neighbors=15)
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.5)

# Visualize
sc.pl.umap(adata, color=['leiden', 'batch'], ncols=2)
Raw lines 158 to 172
173## Step 7: Differential Accessibility
175```python
176# Differential accessibility between clusters
177da_results = model.differential_accessibility(
178 groupby='leiden',
179 group1='0',
180 group2='1'
183# Filter significant peaks
184da_sig = da_results[
185 (da_results['is_da_fdr_0.05']) &
186 (abs(da_results['lfc_mean']) > 1)
189print(f"Significant DA peaks: {len(da_sig)}")
190print(da_sig.head())
191```

Step 7: Differential Accessibility

# Differential accessibility between clusters
da_results = model.differential_accessibility(
    groupby='leiden',
    group1='0',
    group2='1'
)

# Filter significant peaks
da_sig = da_results[
    (da_results['is_da_fdr_0.05']) &
    (abs(da_results['lfc_mean']) > 1)
]

print(f"Significant DA peaks: {len(da_sig)}")
print(da_sig.head())
Raw lines 173 to 192
193### DA Between Conditions
195```python
196# Compare conditions within cell type
197adata_subset = adata[adata.obs['cell_type'] == 'CD4 T cells'].copy()
199da_condition = model.differential_accessibility(
200 groupby='condition',
201 group1='treated',
202 group2='control'
204```

DA Between Conditions

# Compare conditions within cell type
adata_subset = adata[adata.obs['cell_type'] == 'CD4 T cells'].copy()

da_condition = model.differential_accessibility(
    groupby='condition',
    group1='treated',
    group2='control'
)
Raw lines 193 to 205
206## Step 8: Peak Annotation
208```python
209# Annotate peaks with nearest genes
210# Using pybedtools or similar
212# Example peak name format: chr1:1000-2000
213# Parse into bed format for annotation
215import pandas as pd
217def parse_peak_names(peak_names):
218 """Parse peak names into bed format."""
219 records = []
220 for peak in peak_names:
221 chrom, coords = peak.split(':')
222 start, end = coords.split('-')
223 records.append({
224 'chrom': chrom,
225 'start': int(start),
226 'end': int(end),
227 'peak': peak
228 })
229 return pd.DataFrame(records)
231peak_bed = parse_peak_names(adata.var_names)
232```

Step 8: Peak Annotation

# Annotate peaks with nearest genes
# Using pybedtools or similar

# Example peak name format: chr1:1000-2000
# Parse into bed format for annotation

import pandas as pd

def parse_peak_names(peak_names):
    """Parse peak names into bed format."""
    records = []
    for peak in peak_names:
        chrom, coords = peak.split(':')
        start, end = coords.split('-')
        records.append({
            'chrom': chrom,
            'start': int(start),
            'end': int(end),
            'peak': peak
        })
    return pd.DataFrame(records)

peak_bed = parse_peak_names(adata.var_names)
Raw lines 206 to 233
234## Step 9: Motif Analysis
236```python
237# Export significant peaks for motif analysis
238# Use HOMER, MEME, or chromVAR
240# Export peak sequences
241sig_peaks = da_sig.index.tolist()
242peak_bed_sig = peak_bed[peak_bed['peak'].isin(sig_peaks)]
243peak_bed_sig.to_csv("significant_peaks.bed", sep='\t', index=False, header=False)
245# Then run HOMER:
246# findMotifsGenome.pl significant_peaks.bed hg38 motif_output/ -size 200
247```

Step 9: Motif Analysis

# Export significant peaks for motif analysis
# Use HOMER, MEME, or chromVAR

# Export peak sequences
sig_peaks = da_sig.index.tolist()
peak_bed_sig = peak_bed[peak_bed['peak'].isin(sig_peaks)]
peak_bed_sig.to_csv("significant_peaks.bed", sep='\t', index=False, header=False)

# Then run HOMER:
# findMotifsGenome.pl significant_peaks.bed hg38 motif_output/ -size 200
Raw lines 234 to 248
249## Step 10: Gene Activity Scores
251```python
252# Compute gene activity from peak accessibility
253# (Requires peak-gene annotations)
255def compute_gene_activity(adata, peak_gene_map):
256 """
257 Compute gene activity scores from peak accessibility.
259 Parameters
260 ----------
261 adata : AnnData
262 ATAC data with peaks
263 peak_gene_map : dict
264 Mapping of peaks to genes
266 Returns
267 -------
268 AnnData with gene activity scores
269 """
270 from scipy.sparse import csr_matrix
272 genes = list(set(peak_gene_map.values()))
273 gene_matrix = np.zeros((adata.n_obs, len(genes)))
275 for i, gene in enumerate(genes):
276 gene_peaks = [p for p, g in peak_gene_map.items() if g == gene]
277 if gene_peaks:
278 peak_idx = [list(adata.var_names).index(p) for p in gene_peaks if p in adata.var_names]
279 if peak_idx:
280 gene_matrix[:, i] = np.array(adata.X[:, peak_idx].sum(axis=1)).flatten()
282 adata_gene = ad.AnnData(
283 X=csr_matrix(gene_matrix),
284 obs=adata.obs.copy(),
285 var=pd.DataFrame(index=genes)
286 )
288 return adata_gene
289```

Step 10: Gene Activity Scores

# Compute gene activity from peak accessibility
# (Requires peak-gene annotations)

def compute_gene_activity(adata, peak_gene_map):
    """
    Compute gene activity scores from peak accessibility.
    
    Parameters
    ----------
    adata : AnnData
        ATAC data with peaks
    peak_gene_map : dict
        Mapping of peaks to genes
        
    Returns
    -------
    AnnData with gene activity scores
    """
    from scipy.sparse import csr_matrix
    
    genes = list(set(peak_gene_map.values()))
    gene_matrix = np.zeros((adata.n_obs, len(genes)))
    
    for i, gene in enumerate(genes):
        gene_peaks = [p for p, g in peak_gene_map.items() if g == gene]
        if gene_peaks:
            peak_idx = [list(adata.var_names).index(p) for p in gene_peaks if p in adata.var_names]
            if peak_idx:
                gene_matrix[:, i] = np.array(adata.X[:, peak_idx].sum(axis=1)).flatten()
    
    adata_gene = ad.AnnData(
        X=csr_matrix(gene_matrix),
        obs=adata.obs.copy(),
        var=pd.DataFrame(index=genes)
    )
    
    return adata_gene
Raw lines 249 to 290
291## Complete Pipeline
293```python
294def analyze_scatac(
295 adata,
296 batch_key=None,
297 n_top_peaks=50000,
298 n_latent=20,
299 resolution=0.5
300):
301 """
302 Complete scATAC-seq analysis with PeakVI.
304 Parameters
305 ----------
306 adata : AnnData
307 Raw peak-cell matrix
308 batch_key : str, optional
309 Batch annotation column
310 n_top_peaks : int
311 Number of top peaks to use
312 n_latent : int
313 Latent dimensions
314 resolution : float
315 Leiden clustering resolution
317 Returns
318 -------
319 Tuple of (processed AnnData, trained model)
320 """
321 import scvi
322 import scanpy as sc
323 import numpy as np
325 adata = adata.copy()
327 # QC
328 sc.pp.calculate_qc_metrics(adata, inplace=True)
329 adata = adata[adata.obs['n_genes_by_counts'] > 500].copy()
330 sc.pp.filter_genes(adata, min_cells=10)
332 # Binarize
333 adata.X = (adata.X > 0).astype(np.float32)
335 # Select top peaks
336 if adata.n_vars > n_top_peaks:
337 peak_accessibility = np.array(adata.X.sum(axis=0)).flatten()
338 top_peaks = np.argsort(peak_accessibility)[-n_top_peaks:]
339 adata = adata[:, top_peaks].copy()
341 # Setup PeakVI
342 scvi.model.PEAKVI.setup_anndata(adata, batch_key=batch_key)
344 # Train
345 model = scvi.model.PEAKVI(adata, n_latent=n_latent)
346 model.train(max_epochs=200, early_stopping=True)
348 # Latent representation
349 adata.obsm["X_PeakVI"] = model.get_latent_representation()
351 # Clustering
352 sc.pp.neighbors(adata, use_rep="X_PeakVI")
353 sc.tl.umap(adata)
354 sc.tl.leiden(adata, resolution=resolution)
356 return adata, model
358# Usage
359adata, model = analyze_scatac(
360 adata,
361 batch_key="sample",
362 n_top_peaks=50000
365# Visualize
366sc.pl.umap(adata, color=['leiden', 'sample'])
368# Differential accessibility
369da_results = model.differential_accessibility(
370 groupby='leiden',
371 group1='0',
372 group2='1'
374```

Complete Pipeline

def analyze_scatac(
    adata,
    batch_key=None,
    n_top_peaks=50000,
    n_latent=20,
    resolution=0.5
):
    """
    Complete scATAC-seq analysis with PeakVI.
    
    Parameters
    ----------
    adata : AnnData
        Raw peak-cell matrix
    batch_key : str, optional
        Batch annotation column
    n_top_peaks : int
        Number of top peaks to use
    n_latent : int
        Latent dimensions
    resolution : float
        Leiden clustering resolution
        
    Returns
    -------
    Tuple of (processed AnnData, trained model)
    """
    import scvi
    import scanpy as sc
    import numpy as np
    
    adata = adata.copy()
    
    # QC
    sc.pp.calculate_qc_metrics(adata, inplace=True)
    adata = adata[adata.obs['n_genes_by_counts'] > 500].copy()
    sc.pp.filter_genes(adata, min_cells=10)
    
    # Binarize
    adata.X = (adata.X > 0).astype(np.float32)
    
    # Select top peaks
    if adata.n_vars > n_top_peaks:
        peak_accessibility = np.array(adata.X.sum(axis=0)).flatten()
        top_peaks = np.argsort(peak_accessibility)[-n_top_peaks:]
        adata = adata[:, top_peaks].copy()
    
    # Setup PeakVI
    scvi.model.PEAKVI.setup_anndata(adata, batch_key=batch_key)
    
    # Train
    model = scvi.model.PEAKVI(adata, n_latent=n_latent)
    model.train(max_epochs=200, early_stopping=True)
    
    # Latent representation
    adata.obsm["X_PeakVI"] = model.get_latent_representation()
    
    # Clustering
    sc.pp.neighbors(adata, use_rep="X_PeakVI")
    sc.tl.umap(adata)
    sc.tl.leiden(adata, resolution=resolution)
    
    return adata, model

# Usage
adata, model = analyze_scatac(
    adata,
    batch_key="sample",
    n_top_peaks=50000
)

# Visualize
sc.pl.umap(adata, color=['leiden', 'sample'])

# Differential accessibility
da_results = model.differential_accessibility(
    groupby='leiden',
    group1='0',
    group2='1'
)
Raw lines 291 to 375
376## Integration with scRNA-seq
378For multiome data or separate RNA/ATAC from same cells:
380```python
381# See MultiVI for joint RNA+ATAC analysis
382# Or use WNN (weighted nearest neighbors) approach
384# Transfer labels from RNA to ATAC using shared latent space
385```

Integration with scRNA-seq

For multiome data or separate RNA/ATAC from same cells:

# See MultiVI for joint RNA+ATAC analysis
# Or use WNN (weighted nearest neighbors) approach

# Transfer labels from RNA to ATAC using shared latent space
Raw lines 376 to 386
387## Troubleshooting
389| Issue | Cause | Solution |
390|-------|-------|----------|
391| Training slow | Too many peaks | Subset to top 50k peaks |
392| Poor clustering | Too few informative peaks | Use variable peaks |
393| Batch dominates | Strong technical effects | Ensure batch_key is set |
394| Memory error | Large peak matrix | Use sparse format, reduce peaks |

Troubleshooting

IssueCauseSolution
Training slowToo many peaksSubset to top 50k peaks
Poor clusteringToo few informative peaksUse variable peaks
Batch dominatesStrong technical effectsEnsure batch_key is set
Memory errorLarge peak matrixUse sparse format, reduce peaks
Raw lines 387 to 395
396## Key References
398- Ashuach et al. (2022) "PeakVI: A deep generative model for single-cell chromatin accessibility analysis"

Key References

  • Ashuach et al. (2022) "PeakVI: A deep generative model for single-cell chromatin accessibility analysis"
Raw lines 396 to 398