439 lines
11 KiB
Markdown
439 lines
11 KiB
Markdown
---
|
|
title: "Sequence Alignment Analysis"
|
|
task: ""
|
|
lineage_type: import
|
|
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-phylogenetics/references/sequence_alignment.md
|
|
upstream_sha: e2520a96
|
|
imported_at: 2026-06-26
|
|
prompt_class: prompt
|
|
upstream_changes: accepted
|
|
author: upstream
|
|
validated: false
|
|
---
|
|
|
|
# Sequence Alignment Analysis
|
|
|
|
Detailed guide for analyzing multiple sequence alignments using Biopython and PhyKIT.
|
|
|
|
---
|
|
|
|
## File Loading and Format Detection
|
|
|
|
### Auto-detect Alignment Format
|
|
|
|
```python
|
|
from Bio import AlignIO
|
|
|
|
def load_alignment(filepath):
|
|
"""Load alignment with auto-format detection.
|
|
|
|
Supports: FASTA, PHYLIP, PHYLIP-relaxed, Nexus, Clustal, Stockholm
|
|
"""
|
|
# Try phylip-relaxed BEFORE phylip to avoid misparse of long names
|
|
formats_to_try = ['fasta', 'phylip-relaxed', 'phylip', 'nexus', 'clustal', 'stockholm']
|
|
|
|
for fmt in formats_to_try:
|
|
try:
|
|
alignment = AlignIO.read(filepath, fmt)
|
|
return alignment, fmt
|
|
except Exception:
|
|
continue
|
|
|
|
raise ValueError(f"Cannot parse alignment: {filepath}")
|
|
```
|
|
|
|
### Usage
|
|
|
|
```python
|
|
# Load any alignment format
|
|
alignment, format_detected = load_alignment("gene1.fa")
|
|
print(f"Format: {format_detected}")
|
|
print(f"Sequences: {len(alignment)}")
|
|
print(f"Length: {alignment.get_alignment_length()}")
|
|
```
|
|
|
|
---
|
|
|
|
## Parsimony Informative Sites
|
|
|
|
### PhyKIT Implementation
|
|
|
|
```python
|
|
from phykit.services.alignment.parsimony_informative_sites import ParsimonyInformative
|
|
from types import SimpleNamespace
|
|
|
|
def phykit_parsimony_informative(aln_file):
|
|
"""Calculate parsimony informative sites.
|
|
|
|
Parsimony informative site: A site with at least 2 different characters
|
|
appearing at least 2 times each (excluding gaps).
|
|
|
|
Returns: (pi_sites_count, alignment_length, pi_percentage)
|
|
"""
|
|
pi = ParsimonyInformative(SimpleNamespace(alignment=aln_file))
|
|
alignment, _, _ = pi.get_alignment_and_format()
|
|
return pi.calculate_parsimony_informative_sites(alignment)
|
|
```
|
|
|
|
### Example
|
|
|
|
```python
|
|
# Compute parsimony informative sites
|
|
pi_count, aln_len, pi_pct = phykit_parsimony_informative("alignment.fa")
|
|
|
|
print(f"Parsimony informative sites: {pi_count}")
|
|
print(f"Alignment length: {aln_len}")
|
|
print(f"Percentage: {pi_pct:.4f}%")
|
|
```
|
|
|
|
### Batch Processing
|
|
|
|
```python
|
|
def batch_parsimony_informative(gene_files):
|
|
"""Compute PI sites for multiple genes."""
|
|
results = {}
|
|
for entry in gene_files:
|
|
if 'aln_file' in entry:
|
|
try:
|
|
results[entry['gene_id']] = phykit_parsimony_informative(entry['aln_file'])
|
|
except Exception:
|
|
pass # Skip genes that fail
|
|
return results
|
|
|
|
# Usage
|
|
pi_results = batch_parsimony_informative(gene_files)
|
|
|
|
# Extract just counts
|
|
pi_counts = {gene: result[0] for gene, result in pi_results.items()}
|
|
|
|
# Extract percentages
|
|
pi_percentages = {gene: result[2] for gene, result in pi_results.items()}
|
|
```
|
|
|
|
---
|
|
|
|
## Relative Composition Variability (RCV)
|
|
|
|
### PhyKIT Implementation
|
|
|
|
```python
|
|
from phykit.services.alignment.rcv import RelativeCompositionVariability
|
|
|
|
def phykit_rcv(aln_file):
|
|
"""Calculate Relative Composition Variability.
|
|
|
|
RCV measures compositional heterogeneity across sequences.
|
|
Lower values indicate more uniform base composition.
|
|
|
|
Returns: float (RCV score)
|
|
"""
|
|
rcv = RelativeCompositionVariability(SimpleNamespace(alignment=aln_file))
|
|
return rcv.calculate_rcv()
|
|
```
|
|
|
|
### Example
|
|
|
|
```python
|
|
# Single alignment
|
|
rcv_score = phykit_rcv("alignment.fa")
|
|
print(f"RCV: {rcv_score:.4f}")
|
|
|
|
# Batch processing
|
|
def batch_rcv(gene_files):
|
|
return batch_compute_metric(gene_files, phykit_rcv, requires='alignment')
|
|
|
|
rcv_results = batch_rcv(gene_files)
|
|
```
|
|
|
|
---
|
|
|
|
## Alignment Gap Analysis
|
|
|
|
### Gap Percentage
|
|
|
|
```python
|
|
import numpy as np
|
|
|
|
def alignment_gap_percentage(aln_file):
|
|
"""Calculate overall gap percentage in alignment.
|
|
|
|
Gap characters: '-', '.', '?'
|
|
|
|
Returns: percentage (0-100)
|
|
"""
|
|
alignment, fmt = load_alignment(aln_file)
|
|
n_seqs = len(alignment)
|
|
aln_len = alignment.get_alignment_length()
|
|
total_chars = n_seqs * aln_len
|
|
|
|
# Convert to numpy array
|
|
arr = np.array([[c for c in str(rec.seq)] for rec in alignment])
|
|
|
|
# Count gaps
|
|
gap_count = np.sum(np.isin(arr, ['-', '.', '?']))
|
|
|
|
return (gap_count / total_chars) * 100
|
|
```
|
|
|
|
### Per-Sequence Gap Statistics
|
|
|
|
```python
|
|
def per_sequence_gaps(aln_file):
|
|
"""Calculate gap percentage for each sequence."""
|
|
alignment, _ = load_alignment(aln_file)
|
|
|
|
results = []
|
|
for record in alignment:
|
|
seq_str = str(record.seq)
|
|
gap_count = seq_str.count('-') + seq_str.count('.') + seq_str.count('?')
|
|
gap_pct = (gap_count / len(seq_str)) * 100
|
|
results.append({
|
|
'seq_id': record.id,
|
|
'gap_count': gap_count,
|
|
'gap_percentage': gap_pct
|
|
})
|
|
|
|
return results
|
|
```
|
|
|
|
---
|
|
|
|
## Comprehensive Alignment Statistics
|
|
|
|
### All-in-One Function
|
|
|
|
```python
|
|
def alignment_statistics(aln_file):
|
|
"""Comprehensive alignment statistics.
|
|
|
|
Returns dict with:
|
|
- n_seqs: Number of sequences
|
|
- aln_len: Alignment length
|
|
- gap_pct: Gap percentage
|
|
- gc_pct: GC content
|
|
- pi_sites: Parsimony informative sites count
|
|
- pi_pct: Parsimony informative sites percentage
|
|
- variable_sites: Number of variable sites
|
|
- variable_pct: Variable sites percentage
|
|
"""
|
|
alignment, fmt = load_alignment(aln_file)
|
|
n_seqs = len(alignment)
|
|
aln_len = alignment.get_alignment_length()
|
|
|
|
# Convert to numpy array
|
|
arr = np.array([[c.upper() for c in str(rec.seq)] for rec in alignment])
|
|
|
|
# Gap percentage
|
|
gap_count = np.sum(np.isin(arr, ['-', '.', '?']))
|
|
gap_pct = (gap_count / (n_seqs * aln_len)) * 100
|
|
|
|
# GC content (excluding gaps)
|
|
non_gap = arr[~np.isin(arr, ['-', '.', '?', 'N'])]
|
|
gc_count = np.sum(np.isin(non_gap, ['G', 'C']))
|
|
gc_pct = (gc_count / len(non_gap)) * 100 if len(non_gap) > 0 else 0
|
|
|
|
# Parsimony informative sites
|
|
pi_sites, _, pi_pct = phykit_parsimony_informative(aln_file)
|
|
|
|
# Variable sites
|
|
variable_count = 0
|
|
for i in range(aln_len):
|
|
col = arr[:, i]
|
|
non_gap_col = col[~np.isin(col, ['-', '.', '?'])]
|
|
if len(np.unique(non_gap_col)) > 1:
|
|
variable_count += 1
|
|
|
|
return {
|
|
'n_seqs': n_seqs,
|
|
'aln_len': aln_len,
|
|
'gap_pct': round(gap_pct, 4),
|
|
'gc_pct': round(gc_pct, 4),
|
|
'pi_sites': pi_sites,
|
|
'pi_pct': round(pi_pct, 4),
|
|
'variable_sites': variable_count,
|
|
'variable_pct': round((variable_count / aln_len) * 100, 4)
|
|
}
|
|
```
|
|
|
|
### Example
|
|
|
|
```python
|
|
# Get comprehensive stats
|
|
stats = alignment_statistics("alignment.fa")
|
|
|
|
print(f"Sequences: {stats['n_seqs']}")
|
|
print(f"Length: {stats['aln_len']}")
|
|
print(f"Gaps: {stats['gap_pct']:.2f}%")
|
|
print(f"GC: {stats['gc_pct']:.2f}%")
|
|
print(f"PI sites: {stats['pi_sites']} ({stats['pi_pct']:.2f}%)")
|
|
print(f"Variable sites: {stats['variable_sites']} ({stats['variable_pct']:.2f}%)")
|
|
```
|
|
|
|
---
|
|
|
|
## Format Conversion
|
|
|
|
### Convert Between Formats
|
|
|
|
```python
|
|
def convert_alignment_format(input_file, output_file, output_format='fasta'):
|
|
"""Convert alignment to different format.
|
|
|
|
Supported formats: fasta, phylip, phylip-relaxed, nexus, clustal
|
|
"""
|
|
alignment, input_format = load_alignment(input_file)
|
|
AlignIO.write(alignment, output_file, output_format)
|
|
return output_format
|
|
```
|
|
|
|
### Example
|
|
|
|
```python
|
|
# Convert PHYLIP to FASTA
|
|
convert_alignment_format("alignment.phy", "alignment.fa", "fasta")
|
|
|
|
# Convert FASTA to Nexus
|
|
convert_alignment_format("alignment.fa", "alignment.nex", "nexus")
|
|
```
|
|
|
|
---
|
|
|
|
## Filtering Alignments
|
|
|
|
### By Gap Threshold
|
|
|
|
```python
|
|
def filter_by_gap_threshold(gene_files, max_gap_pct=5.0):
|
|
"""Filter alignments by maximum gap percentage.
|
|
|
|
Returns: list of gene entries passing threshold
|
|
"""
|
|
valid_genes = []
|
|
for entry in gene_files:
|
|
if 'aln_file' in entry:
|
|
try:
|
|
gap_pct = alignment_gap_percentage(entry['aln_file'])
|
|
if gap_pct <= max_gap_pct:
|
|
valid_genes.append(entry)
|
|
except Exception:
|
|
pass
|
|
return valid_genes
|
|
```
|
|
|
|
### By Minimum Sequences
|
|
|
|
```python
|
|
def filter_by_min_sequences(gene_files, min_seqs=4):
|
|
"""Filter alignments by minimum number of sequences."""
|
|
valid_genes = []
|
|
for entry in gene_files:
|
|
if 'aln_file' in entry:
|
|
try:
|
|
alignment, _ = load_alignment(entry['aln_file'])
|
|
if len(alignment) >= min_seqs:
|
|
valid_genes.append(entry)
|
|
except Exception:
|
|
pass
|
|
return valid_genes
|
|
```
|
|
|
|
### Combined Filtering
|
|
|
|
```python
|
|
# Filter by multiple criteria
|
|
valid_genes = gene_files
|
|
valid_genes = filter_by_gap_threshold(valid_genes, max_gap_pct=5.0)
|
|
valid_genes = filter_by_min_sequences(valid_genes, min_seqs=4)
|
|
|
|
print(f"Genes passing filters: {len(valid_genes)}/{len(gene_files)}")
|
|
```
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Pattern: Median Parsimony Sites
|
|
|
|
```python
|
|
# Question: "What is the median number of parsimony informative sites?"
|
|
|
|
# 1. Batch compute
|
|
pi_results = batch_parsimony_informative(gene_files)
|
|
|
|
# 2. Extract counts
|
|
pi_counts = [result[0] for result in pi_results.values()]
|
|
|
|
# 3. Compute median
|
|
median_pi = np.median(pi_counts)
|
|
|
|
print(f"Median PI sites: {median_pi:.4f}")
|
|
```
|
|
|
|
### Pattern: Percentage Above Threshold
|
|
|
|
```python
|
|
# Question: "What percentage of alignments have >100 PI sites?"
|
|
|
|
pi_counts = [result[0] for result in pi_results.values()]
|
|
above_threshold = sum(1 for count in pi_counts if count > 100)
|
|
percentage = (above_threshold / len(pi_counts)) * 100
|
|
|
|
print(f"Percentage > 100 PI sites: {percentage:.2f}%")
|
|
```
|
|
|
|
### Pattern: Ratio of Minimums
|
|
|
|
```python
|
|
# Question: "What is the ratio of minimum PI sites (fungi / animals)?"
|
|
|
|
fungi_pi = batch_parsimony_informative(fungi_genes)
|
|
animal_pi = batch_parsimony_informative(animal_genes)
|
|
|
|
fungi_counts = [r[0] for r in fungi_pi.values()]
|
|
animal_counts = [r[0] for r in animal_pi.values()]
|
|
|
|
ratio = min(fungi_counts) / min(animal_counts)
|
|
|
|
print(f"Ratio of minimums: {ratio:.4f}")
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### "Cannot parse alignment" Error
|
|
|
|
**Cause**: Format detection failed.
|
|
|
|
**Solution**: Try specifying format explicitly:
|
|
```python
|
|
alignment = AlignIO.read("file.phy", "phylip-relaxed")
|
|
```
|
|
|
|
### Long Sequence Names in PHYLIP
|
|
|
|
**Issue**: PHYLIP format truncates names to 10 characters.
|
|
|
|
**Solution**: Use `phylip-relaxed` format:
|
|
```python
|
|
alignment = AlignIO.read("file.phy", "phylip-relaxed")
|
|
```
|
|
|
|
### Empty PI Sites
|
|
|
|
**Cause**: Alignment lacks informative sites (too few sequences or too conserved).
|
|
|
|
**Solution**: Check alignment quality:
|
|
```python
|
|
stats = alignment_statistics("alignment.fa")
|
|
print(f"Variable sites: {stats['variable_sites']}")
|
|
```
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- `tree_building.md` - Tree construction from alignments
|
|
- `parsimony_analysis.md` - Statistical comparison workflows
|
|
- `scripts/format_alignment.py` - Format conversion utilities
|