11 KiB
11 KiB
title, task, lineage_type, upstream_source, upstream_sha, imported_at, prompt_class, upstream_changes, author, validated
| title | task | lineage_type | upstream_source | upstream_sha | imported_at | prompt_class | upstream_changes | author | validated |
|---|---|---|---|---|---|---|---|---|---|
| Sequence Alignment Analysis | import | https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-phylogenetics/references/sequence_alignment.md | e2520a96 | 2026-06-26 | prompt | accepted | upstream | false |
Sequence Alignment Analysis
Detailed guide for analyzing multiple sequence alignments using Biopython and PhyKIT.
File Loading and Format Detection
Auto-detect Alignment Format
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
# 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
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
# 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
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
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
# 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
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
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
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
# 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
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
# 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
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
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
# 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
# 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
# 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
# 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:
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:
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:
stats = alignment_statistics("alignment.fa")
print(f"Variable sites: {stats['variable_sites']}")
See Also
tree_building.md- Tree construction from alignmentsparsimony_analysis.md- Statistical comparison workflowsscripts/format_alignment.py- Format conversion utilities