alterlab-pysam
Read and write genomic alignment and variant files in Python with pysam (htslib bindings) — SAM/BAM/CRAM alignments, VCF/BCF variants, and FASTA/FASTQ sequences, plus region extraction and per-base coverage/pileup. Use when scripting NGS data-processing pipelines that parse, filt
Install
npx skills add https://github.com/AlterLab-IEU/AlterLab-Academic-Skills/tree/main/skills/bioinformatics/alterlab-pysam
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alterlab-ieu-alterlab-academic-skills@llmmart
git clone https://github.com/AlterLab-IEU/AlterLab-Academic-Skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alterlab-ieu/alterlab-academic-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Pysam
Overview
Pysam is a Python module for reading, manipulating, and writing genomic datasets. Read/write SAM/BAM/CRAM alignment files, VCF/BCF variant files, and FASTA/FASTQ sequences with a Pythonic interface to htslib. Query tabix-indexed files, perform pileup analysis for coverage, and execute samtools/bcftools commands.
When to Use This Skill
This skill should be used when:
- Working with sequencing alignment files (BAM/CRAM)
- Analyzing genetic variants (VCF/BCF)
- Extracting reference sequences or gene regions
- Processing raw sequencing data (FASTQ)
- Calculating coverage or read depth
- Implementing bioinformatics analysis pipelines
- Quality control of sequencing data
- Variant calling and annotation workflows
Does NOT Trigger
| Scenario | Use Instead |
|---|---|
| Running a whole variant-calling pipeline (alignment -> GATK/DeepVariant -> annotation) | alterlab-nf-core-sarek |
| Storing and querying many samples' variants as a queryable array | alterlab-tiledbvcf |
| Coverage tracks, bigWig generation, and deepTools-style BAM summaries | alterlab-deeptools |
| Sequence-record parsing, translation, alignment objects (no BAM/VCF) | alterlab-biopython |
| Transcript-level quantification from RNA-seq FASTQ | alterlab-rnaseq-quant |
Quick Start
Installation
uv pip install pysam
Basic Examples
Read alignment file:
import pysam
# Open BAM file and fetch reads in region
samfile = pysam.AlignmentFile("example.bam", "rb")
for read in samfile.fetch("chr1", 1000, 2000):
print(f"{read.query_name}: {read.reference_start}")
samfile.close()
Read variant file:
# Open VCF file and iterate variants
vcf = pysam.VariantFile("variants.vcf")
for variant in vcf:
print(f"{variant.chrom}:{variant.pos} {variant.ref}>{variant.alts}")
vcf.close()
Query reference sequence:
# Open FASTA and extract sequence
fasta = pysam.FastaFile("reference.fasta")
sequence = fasta.fetch("chr1", 1000, 2000)
print(sequence)
fasta.close()
Core Capabilities
1. Alignment File Operations (SAM/BAM/CRAM)
Use the AlignmentFile class to work with aligned sequencing reads. This is appropriate for analyzing mapping results, calculating coverage, extracting reads, or quality control.
Common operations:
- Open and read BAM/SAM/CRAM files
- Fetch reads from specific genomic regions
- Filter reads by mapping quality, flags, or other criteria
- Write filtered or modified alignments
- Calculate coverage statistics
- Perform pileup analysis (base-by-base coverage)
- Access read sequences, quality scores, and alignment information
Reference: See references/alignment_files.md for detailed documentation on:
- Opening and reading alignment files
- AlignedSegment attributes and methods
- Region-based fetching with
fetch() - Pileup analysis for coverage
- Writing and creating BAM files
- Coordinate systems and indexing
- Performance optimization tips
2. Variant File Operations (VCF/BCF)
Use the VariantFile class to work with genetic variants from variant calling pipelines. This is appropriate for variant analysis, filtering, annotation, or population genetics.
Common operations:
- Read and write VCF/BCF files
- Query variants in specific regions
- Access variant information (position, alleles, quality)
- Extract genotype data for samples
- Filter variants by quality, allele frequency, or other criteria
- Annotate variants with additional information
- Subset samples or regions
Reference: See references/variant_files.md for detailed documentation on:
- Opening and reading variant files
- VariantRecord attributes and methods
- Accessing INFO and FORMAT fields
- Working with genotypes and samples
- Creating and writing VCF files
- Filtering and subsetting variants
- Multi-sample VCF operations
3. Sequence File Operations (FASTA/FASTQ)
Use FastaFile for random access to reference sequences and FastxFile for reading raw sequencing data. This is appropriate for extracting gene sequences, validating variants against reference, or processing raw reads.
Common operations:
- Query reference sequences by genomic coordinates
- Extract sequences for genes or regions of interest
- Read FASTQ files with quality scores
- Validate variant reference alleles
- Calculate sequence statistics
- Filter reads by quality or length
- Convert between FASTA and FASTQ formats
Reference: See references/sequence_files.md for detailed documentation on:
- FASTA file access and indexing
- Extracting sequences by region
- Handling reverse complement for genes
- Reading FASTQ files sequentially
- Quality score conversion and filtering
- Working with tabix-indexed files (BED, GTF, GFF)
- Common sequence processing patterns
4. Integrated Bioinformatics Workflows
Pysam excels at integrating multiple file types for comprehensive genomic analyses. Common workflows combine alignment files, variant files, and reference sequences.
Common workflows:
- Calculate coverage statistics for specific regions
- Validate variants against aligned reads
- Annotate variants with coverage information
- Extract sequences around variant positions
- Filter alignments or variants based on multiple criteria
- Generate coverage tracks for visualization
- Quality control across multiple data types
Reference: See references/common_workflows.md for detailed examples of:
- Quality control workflows (BAM statistics, reference consistency)
- Coverage analysis (per-base coverage, low coverage detection)
- Variant analysis (annotation, filtering by read support)
- Sequence extraction (variant contexts, gene sequences)
- Read filtering and subsetting
- Integration patterns (BAM+VCF, VCF+BED, etc.)
- Performance optimization for complex workflows
Key Concepts
Coordinate Systems
Critical: Pysam uses 0-based, half-open coordinates (Python convention):
- Start positions are 0-based (first base is position 0)
- End positions are exclusive (not included in the range)
- Region 1000-2000 includes bases 1000-1999 (1000 bases total)
Exception: Region strings in fetch() follow samtools convention (1-based):
samfile.fetch("chr1", 999, 2000) # 0-based: positions 999-1999
samfile.fetch("chr1:1000-2000") # 1-based string: positions 1000-2000
VCF files: Use 1-based coordinates in the file format, but VariantRecord.start is 0-based.
Indexing Requirements
Random access to specific genomic regions requires index files:
- BAM files: Require
.baiindex (create withpysam.index()) - CRAM files: Require
.craiindex - FASTA files: Require
.faiindex (create withpysam.faidx()) - VCF.gz files: Require
.tbitabix index (create withpysam.tabix_index()) - BCF files: Require
.csiindex
Without an index, use fetch(until_eof=True) for sequential reading.
File Modes
Specify format when opening files:
"rb"- Read BAM (binary)"r"- Read SAM (text)"rc"- Read CRAM"wb"- Write BAM"w"- Write SAM"wc"- Write CRAM
CRAM: two behaviour changes since pysam 0.24 / htslib 1.22
These bite quietly, so check them before blaming your data.
The default output CRAM version is now 3.1, not 3.0. Files written with
"wc"may not be readable by older samtools or by downstream tools pinned to an older htslib. Write 3.0 explicitly when the consumer is out of your control:out = pysam.AlignmentFile("out.cram", "wc", header=src.header, reference_filename="ref.fa", format_options=["version=3.0"])format_optionstakes a list ofstr(it acceptedbytesin older pysam).CRAM reference sequences are no longer fetched from EBI automatically. CRAM stores reads relative to a reference, so reading one without the matching FASTA now fails instead of silently downloading it. Pass
reference_filename=when opening, or set theREF_PATH/REF_CACHEenvironment variables to a local cache.
Performance Considerations
- Always use indexed files for random access operations
- Use
pileup()for column-wise analysis instead of repeated fetch operations - Use
count()for counting instead of iterating and counting manually - Process regions in parallel when analyzing independent genomic regions
- Close files explicitly to free resources
- Use
until_eof=Truefor sequential processing without index - Avoid multiple iterators unless necessary (use
multiple_iterators=Trueif needed)
Common Pitfalls
- Coordinate confusion: Remember 0-based vs 1-based systems in different contexts
- Missing indices: Many operations require index files—create them first
- Partial overlaps:
fetch()returns reads overlapping region boundaries, not just those fully contained - Unbounded pileup:
pileup(chrom, start, stop)yields columns for every position spanned by overlapping reads, not juststart..stop—passtruncate=Trueto restrict to the requested window - Silent base-quality drop:
count_coverage()defaults toquality_threshold=15, so low-quality bases are omitted; setquality_threshold=0to count all - Iterator scope: Keep pileup iterator references alive to avoid "PileupProxy accessed after iterator finished" errors
- Quality score editing: Cannot modify
query_qualitiesin place after changingquery_sequence—create a copy first - Stream limitations: Only stdin/stdout are supported for streaming, not arbitrary Python file objects
- Thread safety: While GIL is released during I/O, comprehensive thread-safety hasn't been fully validated
- CRAM without a reference: see the CRAM note above—
reference_filename=orREF_PATHis now required
Command-Line Tools
Pysam provides access to samtools and bcftools commands:
# Sort BAM file
pysam.samtools.sort("-o", "sorted.bam", "input.bam")
# Index BAM
pysam.samtools.index("sorted.bam")
# View specific region
pysam.samtools.view("-b", "-o", "region.bam", "input.bam", "chr1:1000-2000")
# BCF tools
pysam.bcftools.view("-O", "z", "-o", "output.vcf.gz", "input.vcf")
Error handling:
try:
pysam.samtools.sort("-o", "output.bam", "input.bam")
except pysam.SamtoolsError as e:
print(f"Error: {e}")
Resources
references/
Detailed documentation for each major capability:
alignment_files.md - Complete guide to SAM/BAM/CRAM operations, including AlignmentFile class, AlignedSegment attributes, fetch operations, pileup analysis, and writing alignments
variant_files.md - Complete guide to VCF/BCF operations, including VariantFile class, VariantRecord attributes, genotype handling, INFO/FORMAT fields, and multi-sample operations
sequence_files.md - Complete guide to FASTA/FASTQ operations, including FastaFile and FastxFile classes, sequence extraction, quality score handling, and tabix-indexed file access
common_workflows.md - Practical examples of integrated bioinformatics workflows combining multiple file types, including quality control, coverage analysis, variant validation, and sequence extraction
Getting Help
For detailed information on specific operations, refer to the appropriate reference document:
- Working with BAM files or calculating coverage →
alignment_files.md - Analyzing variants or genotypes →
variant_files.md - Extracting sequences or processing FASTQ →
sequence_files.md - Complex workflows integrating multiple file types →
common_workflows.md
Official documentation: https://pysam.readthedocs.io/
Files (alterlab-academic-skills)
-
evals
-
evals.json 4.2 KB
{ "skill": "alterlab-pysam", "evals": [ { "id": "fetch-reads-region", "prompt": "Open my indexed BAM file example.bam and pull all reads overlapping chr1:1000-2000, printing each read's name and reference start position.", "expected_output": "Invokes alterlab-pysam. Opens the file with pysam.AlignmentFile('example.bam', 'rb'), iterates samfile.fetch('chr1', 999, 2000) (or the 1-based region string), and prints read.query_name and read.reference_start, noting the .bai index requirement and 0-based vs 1-based coordinate convention. Region-based BAM fetching is a core capability.", "assertions": [ { "type": "should_trigger", "value": true }, { "type": "output_contains", "value": "AlignmentFile" } ] }, { "id": "per-base-coverage-pileup", "prompt": "I need per-base read depth across an exon region of a BAM. What's the right pysam approach for column-wise coverage?", "expected_output": "Invokes alterlab-pysam. Uses the pileup() method on an AlignmentFile for column-wise per-base coverage (preferred over repeated fetch), warns to keep the pileup iterator alive to avoid 'PileupProxy accessed after iterator finished', and mentions count()/count_coverage as alternatives. Pileup-based coverage analysis is a documented capability.", "assertions": [ { "type": "should_trigger", "value": true }, { "type": "output_contains", "value": "pileup" } ] }, { "id": "filter-variants-vcf", "prompt": "Read variants.vcf, keep only records with QUAL above 30, and extract the genotype for each sample. Write the kept variants to a new VCF.", "expected_output": "Invokes alterlab-pysam. Opens pysam.VariantFile('variants.vcf'), iterates records filtering on variant.qual, reads sample genotypes from the FORMAT/samples fields, and writes survivors to a new VariantFile opened in write mode using the source header. VCF reading, filtering, genotype access, and writing are in scope.", "assertions": [ { "type": "should_trigger", "value": true }, { "type": "output_contains", "value": "VariantFile" } ] }, { "id": "extract-reference-sequence", "prompt": "From my indexed reference FASTA, extract the sequence for chr1:1000-2000 so I can validate a variant's reference allele.", "expected_output": "Invokes alterlab-pysam. Opens pysam.FastaFile('reference.fasta') (requires the .fai index from pysam.faidx), calls fetch('chr1', 1000, 2000) with 0-based half-open coordinates, and can compare the extracted reference base to a VCF record's ref allele. Random-access reference sequence extraction is a core capability.", "assertions": [ { "type": "should_trigger", "value": true }, { "type": "output_contains", "value": "FastaFile" } ] }, { "id": "near-miss-bioservices", "prompt": "Look up the human BRCA1 protein record in UniProt and map its accession to the matching KEGG and Ensembl IDs across those web databases.", "expected_output": "Does NOT invoke this skill; defers to alterlab-bioservices. The ask is querying and mapping identifiers across remote bioinformatics web databases (UniProt/KEGG/Ensembl), not reading or writing local alignment/variant/sequence files. pysam operates on BAM/CRAM/VCF/FASTA files via htslib, not web services.", "assertions": [ { "type": "should_not_trigger", "value": true }, { "type": "output_contains", "value": "alterlab-bioservices" } ] }, { "id": "near-miss-tiledbvcf", "prompt": "I have thousands of single-sample VCFs from a population cohort and I want to ingest them into a scalable columnar store so I can run fast cross-sample queries over millions of variants.", "expected_output": "Does NOT invoke this skill; defers to alterlab-tiledbvcf. The ask is large-scale cohort VCF ingestion into a scalable variant store for population-level queries, not single-file parsing/filtering with htslib. pysam reads individual VCF/BAM files; tiledbvcf handles the cohort-scale columnar database.", "assertions": [ { "type": "should_not_trigger", "value": true }, { "type": "output_contains", "value": "alterlab-tiledbvcf" } ] } ] }
-
-
references
-
alignment_files.md 9.2 KB
# Working with Alignment Files (SAM/BAM/CRAM) ## Overview Pysam provides the `AlignmentFile` class for reading and writing SAM/BAM/CRAM formatted files containing aligned sequence data. BAM/CRAM files support compression and random access through indexing. ## Opening Alignment Files Specify format via mode qualifier: - `"rb"` - Read BAM (binary) - `"r"` - Read SAM (text) - `"rc"` - Read CRAM (compressed) - `"wb"` - Write BAM - `"w"` - Write SAM - `"wc"` - Write CRAM ```python import pysam # Reading samfile = pysam.AlignmentFile("example.bam", "rb") # Writing (requires template or header) outfile = pysam.AlignmentFile("output.bam", "wb", template=samfile) ``` ### Stream Processing Use `"-"` as filename for stdin/stdout operations: ```python # Read from stdin infile = pysam.AlignmentFile('-', 'rb') # Write to stdout outfile = pysam.AlignmentFile('-', 'w', template=infile) ``` **Important:** Pysam does not support reading/writing from true Python file objects—only stdin/stdout streams are supported. ## AlignmentFile Properties **Header Information:** - `references` - List of chromosome/contig names - `lengths` - Corresponding lengths for each reference - `header` - Complete header as dictionary ```python samfile = pysam.AlignmentFile("example.bam", "rb") print(f"References: {samfile.references}") print(f"Lengths: {samfile.lengths}") ``` ## Reading Reads ### fetch() - Region-Based Retrieval Retrieves reads overlapping specified genomic regions using **0-based coordinates**. ```python # Fetch specific region for read in samfile.fetch("chr1", 1000, 2000): print(read.query_name, read.reference_start) # Fetch entire contig for read in samfile.fetch("chr1"): print(read.query_name) # Fetch without index (sequential read) for read in samfile.fetch(until_eof=True): print(read.query_name) ``` **Important Notes:** - Requires index (.bai/.crai) for random access - Returns reads that **overlap** the region (may extend beyond boundaries) - Use `until_eof=True` for non-indexed files or sequential reading - By default, only returns mapped reads - For unmapped reads, use `fetch("*")` or `until_eof=True` ### Multiple Iterators When using multiple iterators on the same file: ```python samfile = pysam.AlignmentFile("example.bam", "rb", multiple_iterators=True) iter1 = samfile.fetch("chr1", 1000, 2000) iter2 = samfile.fetch("chr2", 5000, 6000) ``` Without `multiple_iterators=True`, a new fetch() call repositions the file pointer and breaks existing iterators. ### count() - Count Reads in Region ```python # Count all reads overlapping the region num_reads = samfile.count("chr1", 1000, 2000) # Count with a filter (count() has no `quality=` arg — use read_callback) num_quality_reads = samfile.count( "chr1", 1000, 2000, read_callback=lambda r: r.mapping_quality >= 20, ) ``` ### count_coverage() - Per-Base Coverage Returns four arrays (A, C, G, T) with per-base coverage: ```python coverage = samfile.count_coverage("chr1", 1000, 2000) a_counts, c_counts, g_counts, t_counts = coverage ``` **Gotcha:** `count_coverage()` defaults to `quality_threshold=15`, so bases with base quality below 15 are silently dropped from the counts. Pass `quality_threshold=0` to count every base regardless of quality. ## AlignedSegment Objects Each read is represented as an `AlignedSegment` object with these key attributes: ### Read Information - `query_name` - Read name/ID - `query_sequence` - Read sequence (bases) - `query_qualities` - Base quality scores (ASCII-encoded) - `query_length` - Length of the read ### Mapping Information - `reference_name` - Chromosome/contig name - `reference_start` - Start position (0-based, inclusive) - `reference_end` - End position (0-based, exclusive) - `mapping_quality` - MAPQ score - `cigarstring` - CIGAR string (e.g., "100M") - `cigartuples` - CIGAR as list of (operation, length) tuples **Important:** `cigartuples` format differs from SAM specification. Operations are integers: - 0 = M (match/mismatch) - 1 = I (insertion) - 2 = D (deletion) - 3 = N (skipped reference) - 4 = S (soft clipping) - 5 = H (hard clipping) - 6 = P (padding) - 7 = = (sequence match) - 8 = X (sequence mismatch) ### Flags and Status - `flag` - SAM flag as integer - `is_paired` - Is read paired? - `is_proper_pair` - Is read in a proper pair? - `is_unmapped` - Is read unmapped? - `mate_is_unmapped` - Is mate unmapped? - `is_reverse` - Is read on reverse strand? - `mate_is_reverse` - Is mate on reverse strand? - `is_read1` - Is this read1? - `is_read2` - Is this read2? - `is_secondary` - Is secondary alignment? - `is_qcfail` - Did read fail QC? - `is_duplicate` - Is read a duplicate? - `is_supplementary` - Is supplementary alignment? ### Tags and Optional Fields - `get_tag(tag)` - Get value of optional field - `set_tag(tag, value)` - Set optional field - `has_tag(tag)` - Check if tag exists - `get_tags()` - Get all tags as list of tuples ```python for read in samfile.fetch("chr1", 1000, 2000): if read.has_tag("NM"): edit_distance = read.get_tag("NM") print(f"{read.query_name}: NM={edit_distance}") ``` ## Writing Alignment Files ### Creating Header ```python header = { 'HD': {'VN': '1.0'}, 'SQ': [ {'LN': 1575, 'SN': 'chr1'}, {'LN': 1584, 'SN': 'chr2'} ] } outfile = pysam.AlignmentFile("output.bam", "wb", header=header) ``` ### Creating AlignedSegment Objects ```python # Create new read a = pysam.AlignedSegment() a.query_name = "read001" a.query_sequence = "AGCTTAGCTAGCTACCTATATCTTGGTCTTGGCCG" a.flag = 0 a.reference_id = 0 # Index into header['SQ'] a.reference_start = 100 a.mapping_quality = 20 a.cigar = [(0, 35)] # 35M a.query_qualities = pysam.qualitystring_to_array("IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII") # Write to file outfile.write(a) ``` ### Converting Between Formats ```python # BAM to SAM infile = pysam.AlignmentFile("input.bam", "rb") outfile = pysam.AlignmentFile("output.sam", "w", template=infile) for read in infile: outfile.write(read) infile.close() outfile.close() ``` ## Pileup Analysis The `pileup()` method provides **column-wise** (position-by-position) analysis across a region: ```python # truncate=True restricts output to the requested region (see note below) for pileupcolumn in samfile.pileup("chr1", 1000, 2000, truncate=True): print(f"Position {pileupcolumn.reference_pos}: coverage = {pileupcolumn.nsegments}") for pileupread in pileupcolumn.pileups: if not pileupread.is_del and not pileupread.is_refskip: # Query position is the position in the read base = pileupread.alignment.query_sequence[pileupread.query_position] print(f" {pileupread.alignment.query_name}: {base}") ``` **Important:** Without `truncate=True`, `pileup()` returns columns for **every** position spanned by any read overlapping the region — including columns outside `start`/`stop`. Pass `truncate=True` to get exactly the requested window instead of manually filtering on the position. Note `nsegments` counts all reads at the column (including those failing default quality filters); it is not strictly equal to `count_coverage()`. **Key attributes:** - `pileupcolumn.reference_pos` - 0-based reference position (`.pos` is a deprecated alias) - `pileupcolumn.nsegments` - Number of reads covering position - `pileupread.alignment` - The AlignedSegment object - `pileupread.query_position` - Position in the read (None for deletions) - `pileupread.is_del` - Is this a deletion? - `pileupread.is_refskip` - Is this a reference skip (N in CIGAR)? **Important:** Keep iterator references alive. The error "PileupProxy accessed after iterator finished" occurs when iterators go out of scope prematurely. ## Coordinate System **Critical:** Pysam uses **0-based, half-open** coordinates (Python convention): - `reference_start` is 0-based (first base is 0) - `reference_end` is exclusive (not included in range) - Region from 1000-2000 includes bases 1000-1999 **Exception:** Region strings in `fetch()` and `pileup()` follow samtools conventions (1-based): ```python # These are equivalent: samfile.fetch("chr1", 999, 2000) # Python style: 0-based samfile.fetch("chr1:1000-2000") # samtools style: 1-based ``` ## Indexing Create BAM index: ```python pysam.index("example.bam") ``` Or use command-line interface: ```python pysam.samtools.index("example.bam") ``` ## Performance Tips 1. **Use indexed access** when querying specific regions repeatedly 2. **Use `pileup()` for column-wise analysis** instead of repeated fetch operations 3. **Use `fetch(until_eof=True)` for sequential reading** of non-indexed files 4. **Avoid multiple iterators** unless necessary (performance cost) 5. **Use `count()` for simple counting** instead of iterating and counting manually ## Common Pitfalls 1. **Partial overlaps:** `fetch()` returns reads that overlap region boundaries—implement explicit filtering if exact boundaries are needed 2. **Quality score editing:** Cannot edit `query_qualities` in place after modifying `query_sequence`. Create a copy first: `quals = read.query_qualities` 3. **Missing index:** `fetch()` without `until_eof=True` requires an index file 4. **Thread safety:** While pysam releases GIL during I/O, comprehensive thread-safety hasn't been fully validated 5. **Iterator scope:** Keep pileup iterator references alive to avoid "PileupProxy accessed after iterator finished" errors -
common_workflows.md 14.9 KB
# Common Bioinformatics Workflows with Pysam ## Overview This document provides practical examples of common bioinformatics workflows using pysam, demonstrating how to combine different file types and operations. ## Quality Control Workflows ### Calculate BAM Statistics ```python import pysam def calculate_bam_stats(bam_file): """Calculate basic statistics for BAM file.""" samfile = pysam.AlignmentFile(bam_file, "rb") stats = { "total_reads": 0, "mapped_reads": 0, "unmapped_reads": 0, "paired_reads": 0, "proper_pairs": 0, "duplicates": 0, "total_bases": 0, "mapped_bases": 0 } for read in samfile.fetch(until_eof=True): stats["total_reads"] += 1 if read.is_unmapped: stats["unmapped_reads"] += 1 else: stats["mapped_reads"] += 1 stats["mapped_bases"] += read.query_alignment_length if read.is_paired: stats["paired_reads"] += 1 if read.is_proper_pair: stats["proper_pairs"] += 1 if read.is_duplicate: stats["duplicates"] += 1 stats["total_bases"] += read.query_length samfile.close() # Calculate derived statistics stats["mapping_rate"] = stats["mapped_reads"] / stats["total_reads"] if stats["total_reads"] > 0 else 0 stats["duplication_rate"] = stats["duplicates"] / stats["total_reads"] if stats["total_reads"] > 0 else 0 return stats ``` ### Check Reference Consistency ```python def check_bam_reference_consistency(bam_file, fasta_file): """Verify that BAM reads match reference genome.""" samfile = pysam.AlignmentFile(bam_file, "rb") fasta = pysam.FastaFile(fasta_file) mismatches = 0 total_checked = 0 for read in samfile.fetch(): if read.is_unmapped: continue # Get reference sequence for aligned region ref_seq = fasta.fetch( read.reference_name, read.reference_start, read.reference_end ) # Get read sequence aligned to reference aligned_pairs = read.get_aligned_pairs(with_seq=True) for query_pos, ref_pos, ref_base in aligned_pairs: if query_pos is not None and ref_pos is not None and ref_base is not None: read_base = read.query_sequence[query_pos] if read_base.upper() != ref_base.upper(): mismatches += 1 total_checked += 1 if total_checked >= 10000: # Sample first 10k positions break samfile.close() fasta.close() error_rate = mismatches / total_checked if total_checked > 0 else 0 return { "positions_checked": total_checked, "mismatches": mismatches, "error_rate": error_rate } ``` ## Coverage Analysis ### Calculate Per-Base Coverage ```python def calculate_coverage(bam_file, chrom, start, end): """Calculate coverage for each position in region.""" samfile = pysam.AlignmentFile(bam_file, "rb") # Initialize coverage array length = end - start coverage = [0] * length # truncate=True restricts columns to the requested window for pileupcolumn in samfile.pileup(chrom, start, end, truncate=True): coverage[pileupcolumn.reference_pos - start] = pileupcolumn.nsegments samfile.close() return coverage ``` ### Identify Low Coverage Regions ```python def find_low_coverage_regions(bam_file, chrom, start, end, min_coverage=10): """Find regions with coverage below threshold.""" samfile = pysam.AlignmentFile(bam_file, "rb") low_coverage_regions = [] in_low_region = False region_start = None for pileupcolumn in samfile.pileup(chrom, start, end, truncate=True): pos = pileupcolumn.reference_pos coverage = pileupcolumn.nsegments if coverage < min_coverage: if not in_low_region: region_start = pos in_low_region = True else: if in_low_region: low_coverage_regions.append((region_start, pos)) in_low_region = False # Close last region if still open if in_low_region: low_coverage_regions.append((region_start, end)) samfile.close() return low_coverage_regions ``` ### Calculate Coverage Statistics ```python def coverage_statistics(bam_file, chrom, start, end): """Calculate coverage statistics for region.""" samfile = pysam.AlignmentFile(bam_file, "rb") coverages = [] for pileupcolumn in samfile.pileup(chrom, start, end, truncate=True): coverages.append(pileupcolumn.nsegments) samfile.close() if not coverages: return None coverages.sort() n = len(coverages) return { "mean": sum(coverages) / n, "median": coverages[n // 2], "min": coverages[0], "max": coverages[-1], "positions": n } ``` ## Variant Analysis ### Extract Variants in Regions ```python def extract_variants_in_genes(vcf_file, bed_file): """Extract variants overlapping gene regions.""" vcf = pysam.VariantFile(vcf_file) bed = pysam.TabixFile(bed_file) variants_by_gene = {} for gene in bed.fetch(parser=pysam.asBed()): gene_name = gene.name variants_by_gene[gene_name] = [] # Find variants in gene region for variant in vcf.fetch(gene.contig, gene.start, gene.end): variant_info = { "chrom": variant.chrom, "pos": variant.pos, "ref": variant.ref, "alt": variant.alts, "qual": variant.qual } variants_by_gene[gene_name].append(variant_info) vcf.close() bed.close() return variants_by_gene ``` ### Annotate Variants with Coverage ```python def annotate_variants_with_coverage(vcf_file, bam_file, output_file): """Add coverage information to variants.""" vcf = pysam.VariantFile(vcf_file) samfile = pysam.AlignmentFile(bam_file, "rb") # Add DP to header if not present if "DP" not in vcf.header.info: vcf.header.info.add("DP", "1", "Integer", "Total Depth from BAM") outvcf = pysam.VariantFile(output_file, "w", header=vcf.header) for variant in vcf: # Get coverage at variant position coverage = samfile.count( variant.chrom, variant.pos - 1, # Convert to 0-based variant.pos ) # Add to INFO field variant.info["DP"] = coverage outvcf.write(variant) vcf.close() samfile.close() outvcf.close() ``` ### Filter Variants by Read Support ```python def filter_variants_by_support(vcf_file, bam_file, output_file, min_alt_reads=3): """Filter variants requiring minimum alternate allele support.""" vcf = pysam.VariantFile(vcf_file) samfile = pysam.AlignmentFile(bam_file, "rb") outvcf = pysam.VariantFile(output_file, "w", header=vcf.header) for variant in vcf: # Count reads supporting each allele allele_counts = {variant.ref: 0} for alt in variant.alts: allele_counts[alt] = 0 # Pileup at variant position (truncate=True yields only this column) for pileupcolumn in samfile.pileup( variant.chrom, variant.pos - 1, variant.pos, truncate=True, ): for pileupread in pileupcolumn.pileups: if not pileupread.is_del and not pileupread.is_refskip: base = pileupread.alignment.query_sequence[ pileupread.query_position ] if base in allele_counts: allele_counts[base] += 1 # Check if any alt allele has sufficient support has_support = any( allele_counts.get(alt, 0) >= min_alt_reads for alt in variant.alts ) if has_support: outvcf.write(variant) vcf.close() samfile.close() outvcf.close() ``` ## Sequence Extraction ### Extract Sequences Around Variants ```python def extract_variant_contexts(vcf_file, fasta_file, output_file, window=50): """Extract reference sequences around variants.""" vcf = pysam.VariantFile(vcf_file) fasta = pysam.FastaFile(fasta_file) with open(output_file, 'w') as out: for variant in vcf: # Get sequence context start = max(0, variant.pos - window - 1) # Convert to 0-based end = variant.pos + window context = fasta.fetch(variant.chrom, start, end) # Mark variant position var_pos_in_context = variant.pos - 1 - start out.write(f">{variant.chrom}:{variant.pos} {variant.ref}>{variant.alts}\n") out.write(context[:var_pos_in_context].lower()) out.write(context[var_pos_in_context:var_pos_in_context+len(variant.ref)].upper()) out.write(context[var_pos_in_context+len(variant.ref):].lower()) out.write("\n") vcf.close() fasta.close() ``` ### Extract Gene Sequences ```python def extract_gene_sequences(bed_file, fasta_file, output_fasta): """Extract sequences for genes from BED file.""" bed = pysam.TabixFile(bed_file) fasta = pysam.FastaFile(fasta_file) with open(output_fasta, 'w') as out: for gene in bed.fetch(parser=pysam.asBed()): sequence = fasta.fetch(gene.contig, gene.start, gene.end) # Handle strand if hasattr(gene, 'strand') and gene.strand == '-': # Reverse complement complement = str.maketrans("ATGCatgcNn", "TACGtacgNn") sequence = sequence.translate(complement)[::-1] out.write(f">{gene.name} {gene.contig}:{gene.start}-{gene.end}\n") # Write sequence in 60-character lines for i in range(0, len(sequence), 60): out.write(sequence[i:i+60] + "\n") bed.close() fasta.close() ``` ## Read Filtering and Subsetting ### Filter BAM by Region and Quality ```python def filter_bam(input_bam, output_bam, chrom, start, end, min_mapq=20): """Filter BAM file by region and mapping quality.""" infile = pysam.AlignmentFile(input_bam, "rb") outfile = pysam.AlignmentFile(output_bam, "wb", template=infile) for read in infile.fetch(chrom, start, end): if read.mapping_quality >= min_mapq and not read.is_duplicate: outfile.write(read) infile.close() outfile.close() # Create index pysam.index(output_bam) ``` ### Extract Reads for Specific Variants ```python def extract_reads_at_variants(bam_file, vcf_file, output_bam, window=100): """Extract reads overlapping variant positions.""" samfile = pysam.AlignmentFile(bam_file, "rb") vcf = pysam.VariantFile(vcf_file) outfile = pysam.AlignmentFile(output_bam, "wb", template=samfile) # Collect all reads (using set to avoid duplicates) reads_to_keep = set() for variant in vcf: start = max(0, variant.pos - window - 1) end = variant.pos + window for read in samfile.fetch(variant.chrom, start, end): reads_to_keep.add(read.query_name) # Write all reads samfile.close() samfile = pysam.AlignmentFile(bam_file, "rb") for read in samfile.fetch(until_eof=True): if read.query_name in reads_to_keep: outfile.write(read) samfile.close() vcf.close() outfile.close() pysam.index(output_bam) ``` ## Integration Workflows ### Create Coverage Track from BAM ```python def create_coverage_bedgraph(bam_file, output_file, chrom=None): """Create bedGraph coverage track from BAM.""" samfile = pysam.AlignmentFile(bam_file, "rb") chroms = [chrom] if chrom else samfile.references with open(output_file, 'w') as out: out.write("track type=bedGraph name=\"Coverage\"\n") for chrom in chroms: current_cov = None region_start = None for pileupcolumn in samfile.pileup(chrom): pos = pileupcolumn.reference_pos cov = pileupcolumn.nsegments if cov != current_cov: # Write previous region if current_cov is not None: out.write(f"{chrom}\t{region_start}\t{pos}\t{current_cov}\n") # Start new region current_cov = cov region_start = pos # Write final region if current_cov is not None: out.write(f"{chrom}\t{region_start}\t{pos+1}\t{current_cov}\n") samfile.close() ``` ### Merge Multiple VCF Files ```python def merge_vcf_samples(vcf_files, output_file): """Merge multiple single-sample VCFs.""" # Open all input files vcf_readers = [pysam.VariantFile(f) for f in vcf_files] # Create merged header merged_header = vcf_readers[0].header.copy() for vcf in vcf_readers[1:]: for sample in vcf.header.samples: merged_header.samples.add(sample) outvcf = pysam.VariantFile(output_file, "w", header=merged_header) # Get all variant positions all_variants = {} for vcf in vcf_readers: for variant in vcf: key = (variant.chrom, variant.pos, variant.ref, variant.alts) if key not in all_variants: all_variants[key] = [] all_variants[key].append(variant) # Write merged variants for key, variants in sorted(all_variants.items()): # Create merged record from first variant merged = outvcf.new_record( contig=variants[0].chrom, start=variants[0].start, stop=variants[0].stop, alleles=variants[0].alleles ) # Add genotypes from all samples for variant in variants: for sample in variant.samples: merged.samples[sample].update(variant.samples[sample]) outvcf.write(merged) # Close all files for vcf in vcf_readers: vcf.close() outvcf.close() ``` ## Performance Tips for Workflows 1. **Use indexed files** for all random access operations 2. **Process regions in parallel** when analyzing multiple independent regions 3. **Stream data when possible** - avoid loading entire files into memory 4. **Close files explicitly** to free resources 5. **Use `until_eof=True`** for sequential processing of entire files 6. **Batch operations** on the same file to minimize I/O 7. **Consider memory usage** with pileup operations on high-coverage regions 8. **Use count() instead of pileup()** when only counts are needed ## Common Integration Patterns 1. **BAM + Reference**: Verify alignments, extract aligned sequences 2. **BAM + VCF**: Validate variants, calculate allele frequencies 3. **VCF + BED**: Annotate variants with gene/region information 4. **BAM + BED**: Calculate coverage statistics for specific regions 5. **FASTA + VCF**: Extract variant context sequences 6. **Multiple BAMs**: Compare coverage or variants across samples 7. **BAM + FASTQ**: Extract unaligned reads for re-alignment -
sequence_files.md 11.3 KB
# Working with Sequence Files (FASTA/FASTQ) ## FASTA Files ### Overview Pysam provides the `FastaFile` class for indexed, random access to FASTA reference sequences. FASTA files must be indexed with `samtools faidx` before use. ### Opening FASTA Files ```python import pysam # Open indexed FASTA file fasta = pysam.FastaFile("reference.fasta") # Automatically looks for reference.fasta.fai index ``` ### Creating FASTA Index ```python # Create index using pysam pysam.faidx("reference.fasta") # Or using samtools command pysam.samtools.faidx("reference.fasta") ``` This creates a `.fai` index file required for random access. ### FastaFile Properties ```python fasta = pysam.FastaFile("reference.fasta") # List of reference sequences references = fasta.references print(f"References: {references}") # Get lengths lengths = fasta.lengths print(f"Lengths: {lengths}") # Get specific sequence length chr1_length = fasta.get_reference_length("chr1") ``` ### Fetching Sequences #### Fetch by Region Uses **0-based, half-open** coordinates: ```python # Fetch specific region sequence = fasta.fetch("chr1", 1000, 2000) print(f"Sequence: {sequence}") # Returns 1000 bases # Fetch entire chromosome chr1_seq = fasta.fetch("chr1") # Fetch using region string (1-based) sequence = fasta.fetch(region="chr1:1001-2000") ``` **Important:** Numeric arguments use 0-based coordinates, region strings use 1-based coordinates (samtools convention). #### Common Use Cases ```python # Get sequence at variant position def get_variant_context(fasta, chrom, pos, window=10): """Get sequence context around a variant position (1-based).""" start = max(0, pos - window - 1) # Convert to 0-based end = pos + window return fasta.fetch(chrom, start, end) # Get sequence for gene coordinates def get_gene_sequence(fasta, chrom, start, end, strand): """Get gene sequence with strand awareness.""" seq = fasta.fetch(chrom, start, end) if strand == "-": # Reverse complement complement = str.maketrans("ATGCatgc", "TACGtacg") seq = seq.translate(complement)[::-1] return seq # Check reference allele def check_ref_allele(fasta, chrom, pos, expected_ref): """Verify reference allele at position (1-based pos).""" actual = fasta.fetch(chrom, pos-1, pos) # Convert to 0-based return actual.upper() == expected_ref.upper() ``` ### Extracting Multiple Regions ```python # Extract multiple regions efficiently regions = [ ("chr1", 1000, 2000), ("chr1", 5000, 6000), ("chr2", 10000, 11000) ] sequences = {} for chrom, start, end in regions: seq_id = f"{chrom}:{start}-{end}" sequences[seq_id] = fasta.fetch(chrom, start, end) ``` ### Working with Ambiguous Bases FASTA files may contain IUPAC ambiguity codes: - N = any base - R = A or G (purine) - Y = C or T (pyrimidine) - S = G or C (strong) - W = A or T (weak) - K = G or T (keto) - M = A or C (amino) - B = C, G, or T (not A) - D = A, G, or T (not C) - H = A, C, or T (not G) - V = A, C, or G (not T) ```python # Handle ambiguous bases def count_ambiguous(sequence): """Count non-ATGC bases.""" return sum(1 for base in sequence.upper() if base not in "ATGC") # Remove regions with too many Ns def has_quality_sequence(fasta, chrom, start, end, max_n_frac=0.1): """Check if region has acceptable N content.""" seq = fasta.fetch(chrom, start, end) n_count = seq.upper().count('N') return (n_count / len(seq)) <= max_n_frac ``` ## FASTQ Files ### Overview Pysam provides `FastxFile` (or `FastqFile`) for reading FASTQ files containing raw sequencing reads with quality scores. FASTQ files do not support random access—only sequential reading. ### Opening FASTQ Files ```python import pysam # Open FASTQ file fastq = pysam.FastxFile("reads.fastq") # Works with compressed files fastq_gz = pysam.FastxFile("reads.fastq.gz") ``` ### Reading FASTQ Records ```python fastq = pysam.FastxFile("reads.fastq") for read in fastq: print(f"Name: {read.name}") print(f"Sequence: {read.sequence}") print(f"Quality: {read.quality}") print(f"Comment: {read.comment}") # Optional header comment ``` **FastqProxy attributes:** - `name` - Read identifier (without @ prefix) - `sequence` - DNA/RNA sequence - `quality` - ASCII-encoded quality string - `comment` - Optional comment from header line - `get_quality_array()` - Convert quality string to numeric array ### Quality Score Conversion ```python # Convert quality string to numeric values for read in fastq: qual_array = read.get_quality_array() mean_quality = sum(qual_array) / len(qual_array) print(f"{read.name}: mean Q = {mean_quality:.1f}") ``` Quality scores are Phred-scaled (typically Phred+33 encoding): - Q = -10 * log10(P_error) - ASCII 33 ('!') = Q0 - ASCII 43 ('+') = Q10 - ASCII 63 ('?') = Q30 ### Common FASTQ Processing Workflows #### Quality Filtering ```python def filter_by_quality(input_fastq, output_fastq, min_mean_quality=20): """Filter reads by mean quality score.""" with pysam.FastxFile(input_fastq) as infile: with open(output_fastq, 'w') as outfile: for read in infile: qual_array = read.get_quality_array() mean_q = sum(qual_array) / len(qual_array) if mean_q >= min_mean_quality: # Write in FASTQ format outfile.write(f"@{read.name}\n") outfile.write(f"{read.sequence}\n") outfile.write("+\n") outfile.write(f"{read.quality}\n") ``` #### Length Filtering ```python def filter_by_length(input_fastq, output_fastq, min_length=50): """Filter reads by minimum length.""" with pysam.FastxFile(input_fastq) as infile: with open(output_fastq, 'w') as outfile: kept = 0 for read in infile: if len(read.sequence) >= min_length: outfile.write(f"@{read.name}\n") outfile.write(f"{read.sequence}\n") outfile.write("+\n") outfile.write(f"{read.quality}\n") kept += 1 print(f"Kept {kept} reads") ``` #### Calculate Quality Statistics ```python def calculate_fastq_stats(fastq_file): """Calculate basic statistics for FASTQ file.""" total_reads = 0 total_bases = 0 quality_sum = 0 with pysam.FastxFile(fastq_file) as fastq: for read in fastq: total_reads += 1 read_length = len(read.sequence) total_bases += read_length qual_array = read.get_quality_array() quality_sum += sum(qual_array) return { "total_reads": total_reads, "total_bases": total_bases, "mean_read_length": total_bases / total_reads if total_reads > 0 else 0, "mean_quality": quality_sum / total_bases if total_bases > 0 else 0 } ``` #### Extract Reads by Name ```python def extract_reads_by_name(fastq_file, read_names, output_file): """Extract specific reads by name.""" read_set = set(read_names) with pysam.FastxFile(fastq_file) as infile: with open(output_file, 'w') as outfile: for read in infile: if read.name in read_set: outfile.write(f"@{read.name}\n") outfile.write(f"{read.sequence}\n") outfile.write("+\n") outfile.write(f"{read.quality}\n") ``` #### Convert FASTQ to FASTA ```python def fastq_to_fasta(fastq_file, fasta_file): """Convert FASTQ to FASTA (discards quality scores).""" with pysam.FastxFile(fastq_file) as infile: with open(fasta_file, 'w') as outfile: for read in infile: outfile.write(f">{read.name}\n") outfile.write(f"{read.sequence}\n") ``` #### Subsample FASTQ ```python import random def subsample_fastq(input_fastq, output_fastq, fraction=0.1, seed=42): """Randomly subsample reads from FASTQ file.""" random.seed(seed) with pysam.FastxFile(input_fastq) as infile: with open(output_fastq, 'w') as outfile: for read in infile: if random.random() < fraction: outfile.write(f"@{read.name}\n") outfile.write(f"{read.sequence}\n") outfile.write("+\n") outfile.write(f"{read.quality}\n") ``` ## Tabix-Indexed Files ### Overview Pysam provides `TabixFile` for accessing tabix-indexed genomic data files (BED, GFF, GTF, generic tab-delimited). ### Opening Tabix Files ```python import pysam # Open tabix-indexed file tabix = pysam.TabixFile("annotations.bed.gz") # File must be bgzip-compressed and tabix-indexed ``` ### Creating Tabix Index ```python # Index a file pysam.tabix_index("annotations.bed", preset="bed", force=True) # Creates annotations.bed.gz and annotations.bed.gz.tbi # Presets available: bed, gff, vcf ``` ### Fetching Records ```python tabix = pysam.TabixFile("annotations.bed.gz") # Fetch region for row in tabix.fetch("chr1", 1000000, 2000000): print(row) # Returns tab-delimited string # Parse with specific parser for row in tabix.fetch("chr1", 1000000, 2000000, parser=pysam.asBed()): print(f"Interval: {row.contig}:{row.start}-{row.end}") # Available parsers: asBed(), asGTF(), asVCF(), asTuple() ``` ### Working with BED Files ```python bed = pysam.TabixFile("regions.bed.gz") # Access BED fields by name for interval in bed.fetch("chr1", 1000000, 2000000, parser=pysam.asBed()): print(f"Region: {interval.contig}:{interval.start}-{interval.end}") print(f"Name: {interval.name}") print(f"Score: {interval.score}") print(f"Strand: {interval.strand}") ``` ### Working with GTF/GFF Files ```python gtf = pysam.TabixFile("annotations.gtf.gz") # Access GTF fields for feature in gtf.fetch("chr1", 1000000, 2000000, parser=pysam.asGTF()): print(f"Feature: {feature.feature}") print(f"Gene: {feature.gene_id}") print(f"Transcript: {feature.transcript_id}") print(f"Coordinates: {feature.start}-{feature.end}") ``` ## Performance Tips ### FASTA 1. **Always use indexed FASTA** files (create .fai with samtools faidx) 2. **Batch fetch operations** when extracting multiple regions 3. **Cache frequently accessed sequences** in memory 4. **Use appropriate window sizes** to avoid loading excessive sequence data ### FASTQ 1. **Stream processing** - FASTQ files are read sequentially, process on-the-fly 2. **Use compressed FASTQ.gz** to save disk space (pysam handles transparently) 3. **Avoid loading entire file** into memory—process read-by-read 4. **For large files**, consider parallel processing with file splitting ### Tabix 1. **Always bgzip and tabix-index** files before region queries 2. **Use appropriate presets** when creating indices 3. **Specify parser** for named field access 4. **Batch queries** to same file to avoid re-opening ## Common Pitfalls 1. **FASTA coordinate system:** fetch() uses 0-based coordinates, region strings use 1-based 2. **Missing index:** FASTA random access requires .fai index file 3. **FASTQ sequential only:** Cannot do random access or region-based queries on FASTQ 4. **Quality encoding:** Assume Phred+33 unless specified otherwise 5. **Tabix compression:** Must use bgzip, not regular gzip, for tabix indexing 6. **Parser requirement:** TabixFile needs explicit parser for named field access 7. **Case sensitivity:** FASTA sequences preserve case—use .upper() or .lower() for consistent comparisons -
variant_files.md 9.3 KB
# Working with Variant Files (VCF/BCF) ## Overview Pysam provides the `VariantFile` class for reading and writing VCF (Variant Call Format) and BCF (binary VCF) files. These files contain information about genetic variants, including SNPs, indels, and structural variants. ## Opening Variant Files ```python import pysam # Reading VCF vcf = pysam.VariantFile("example.vcf") # Reading BCF (binary, compressed) bcf = pysam.VariantFile("example.bcf") # Reading compressed VCF vcf_gz = pysam.VariantFile("example.vcf.gz") # Writing outvcf = pysam.VariantFile("output.vcf", "w", header=vcf.header) ``` ## VariantFile Properties **Header Information:** - `header` - Complete VCF header with metadata - `header.contigs` - Dictionary of contigs/chromosomes - `header.samples` - List of sample names - `header.filters` - Dictionary of FILTER definitions - `header.info` - Dictionary of INFO field definitions - `header.formats` - Dictionary of FORMAT field definitions ```python vcf = pysam.VariantFile("example.vcf") # List samples print(f"Samples: {list(vcf.header.samples)}") # List contigs for contig in vcf.header.contigs: print(f"{contig}: length={vcf.header.contigs[contig].length}") # List INFO fields for info in vcf.header.info: print(f"{info}: {vcf.header.info[info].description}") ``` ## Reading Variant Records ### Iterate All Variants ```python for variant in vcf: print(f"{variant.chrom}:{variant.pos} {variant.ref}>{variant.alts}") ``` ### Fetch Specific Region Requires tabix index (.tbi) for VCF.gz or index for BCF: ```python # Fetch variants in region (1-based coordinates for region string) for variant in vcf.fetch("chr1", 1000000, 2000000): print(f"{variant.chrom}:{variant.pos} {variant.id}") # Using region string (1-based) for variant in vcf.fetch("chr1:1000000-2000000"): print(variant.pos) ``` **Note:** Uses **1-based coordinates** in `fetch()` calls to match VCF specification. ## VariantRecord Objects Each variant is represented as a `VariantRecord` object: ### Position Information - `chrom` - Chromosome/contig name - `pos` - Position (1-based) - `start` - Start position (0-based) - `stop` - Stop position (0-based, exclusive) - `id` - Variant ID (e.g., rsID) ### Allele Information - `ref` - Reference allele - `alts` - Tuple of alternate alleles - `alleles` - Tuple of all alleles (ref + alts) ### Quality and Filtering - `qual` - Quality score (QUAL field) - `filter` - Filter status ### INFO Fields Access INFO fields as dictionary: ```python for variant in vcf: # Check if field exists if "DP" in variant.info: depth = variant.info["DP"] print(f"Depth: {depth}") # Get all INFO keys print(f"INFO fields: {variant.info.keys()}") # Access specific fields if "AF" in variant.info: allele_freq = variant.info["AF"] print(f"Allele frequency: {allele_freq}") ``` ### Sample Genotype Data Access sample data through `samples` dictionary: ```python for variant in vcf: for sample_name in variant.samples: sample = variant.samples[sample_name] # Genotype (GT field) gt = sample["GT"] print(f"{sample_name} genotype: {gt}") # Other FORMAT fields if "DP" in sample: print(f"{sample_name} depth: {sample['DP']}") if "GQ" in sample: print(f"{sample_name} quality: {sample['GQ']}") # Alleles for this genotype alleles = sample.alleles print(f"{sample_name} alleles: {alleles}") # Phasing if sample.phased: print(f"{sample_name} is phased") ``` **Genotype representation:** - `(0, 0)` - Homozygous reference - `(0, 1)` - Heterozygous - `(1, 1)` - Homozygous alternate - `(None, None)` - Missing genotype - Phased: `(0|1)` vs unphased: `(0/1)` ## Writing Variant Files ### Creating Header ```python header = pysam.VariantHeader() # Add contigs header.contigs.add("chr1", length=248956422) header.contigs.add("chr2", length=242193529) # Add INFO fields header.add_line('##INFO=<ID=DP,Number=1,Type=Integer,Description="Total Depth">') header.add_line('##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency">') # Add FORMAT fields header.add_line('##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">') header.add_line('##FORMAT=<ID=DP,Number=1,Type=Integer,Description="Read Depth">') # Add samples header.add_sample("sample1") header.add_sample("sample2") # Create output file outvcf = pysam.VariantFile("output.vcf", "w", header=header) ``` ### Creating Variant Records ```python # Create new variant record = outvcf.new_record() record.chrom = "chr1" record.pos = 100000 record.id = "rs123456" record.ref = "A" record.alts = ("G",) record.qual = 30 record.filter.add("PASS") # Set INFO fields record.info["DP"] = 100 record.info["AF"] = (0.25,) # Set genotype data record.samples["sample1"]["GT"] = (0, 1) record.samples["sample1"]["DP"] = 50 record.samples["sample2"]["GT"] = (0, 0) record.samples["sample2"]["DP"] = 50 # Write to file outvcf.write(record) ``` ## Filtering Variants ### Basic Filtering ```python # Filter by quality for variant in vcf: if variant.qual >= 30: print(f"High quality variant: {variant.chrom}:{variant.pos}") # Filter by depth for variant in vcf: if "DP" in variant.info and variant.info["DP"] >= 20: print(f"High depth variant: {variant.chrom}:{variant.pos}") # Filter by allele frequency for variant in vcf: if "AF" in variant.info: for af in variant.info["AF"]: if af >= 0.01: print(f"Common variant: {variant.chrom}:{variant.pos}") ``` ### Filtering by Genotype ```python # Find variants where sample has alternate allele for variant in vcf: sample = variant.samples["sample1"] gt = sample["GT"] # Check if has alternate allele if gt and any(allele and allele > 0 for allele in gt): print(f"Sample has alt allele: {variant.chrom}:{variant.pos}") # Check if homozygous alternate if gt == (1, 1): print(f"Homozygous alt: {variant.chrom}:{variant.pos}") ``` ### Filter Field ```python # Check FILTER status for variant in vcf: if "PASS" in variant.filter or len(variant.filter) == 0: print(f"Passed filters: {variant.chrom}:{variant.pos}") else: print(f"Failed: {variant.filter.keys()}") ``` ## Indexing VCF Files Create tabix index for compressed VCF: ```python # Compress and index pysam.tabix_index("example.vcf", preset="vcf", force=True) # Creates example.vcf.gz and example.vcf.gz.tbi ``` Or use bcftools for BCF: ```python pysam.bcftools.index("example.bcf") ``` ## Common Workflows ### Extract Variants for Specific Samples ```python invcf = pysam.VariantFile("input.vcf") samples_to_keep = ["sample1", "sample3"] # Create new header with subset of samples new_header = invcf.header.copy() new_header.samples.clear() for sample in samples_to_keep: new_header.samples.add(sample) outvcf = pysam.VariantFile("output.vcf", "w", header=new_header) for variant in invcf: # Create new record new_record = outvcf.new_record( contig=variant.chrom, start=variant.start, stop=variant.stop, alleles=variant.alleles, id=variant.id, qual=variant.qual, filter=variant.filter, info=variant.info ) # Copy genotype data for selected samples for sample in samples_to_keep: new_record.samples[sample].update(variant.samples[sample]) outvcf.write(new_record) ``` ### Calculate Allele Frequencies ```python vcf = pysam.VariantFile("example.vcf") for variant in vcf: total_alleles = 0 alt_alleles = 0 for sample_name in variant.samples: gt = variant.samples[sample_name]["GT"] if gt and None not in gt: total_alleles += 2 alt_alleles += sum(1 for allele in gt if allele > 0) if total_alleles > 0: af = alt_alleles / total_alleles print(f"{variant.chrom}:{variant.pos} AF={af:.4f}") ``` ### Convert VCF to Summary Table ```python import csv vcf = pysam.VariantFile("example.vcf") with open("variants.csv", "w", newline="") as csvfile: writer = csv.writer(csvfile) writer.writerow(["CHROM", "POS", "ID", "REF", "ALT", "QUAL", "DP"]) for variant in vcf: writer.writerow([ variant.chrom, variant.pos, variant.id or ".", variant.ref, ",".join(variant.alts) if variant.alts else ".", variant.qual or ".", variant.info.get("DP", ".") ]) ``` ## Performance Tips 1. **Use BCF format** for better compression and faster access than VCF 2. **Index files** with tabix for efficient region queries 3. **Filter early** to reduce processing of irrelevant variants 4. **Use INFO fields efficiently** - check existence before accessing 5. **Batch write operations** when creating VCF files ## Common Pitfalls 1. **Coordinate systems:** VCF uses 1-based coordinates, but VariantRecord.start is 0-based 2. **Missing data:** Always check if INFO/FORMAT fields exist before accessing 3. **Genotype tuples:** Genotypes are tuples, not lists—handle None values for missing data 4. **Allele indexing:** In genotype (0, 1), 0=REF, 1=first ALT, 2=second ALT, etc. 5. **Index requirement:** Region-based `fetch()` requires tabix index for VCF.gz 6. **Header modification:** When subsetting samples, properly update header and copy FORMAT fields
-
-
SKILL.md 12.4 KB
--- name: alterlab-pysam description: Read and write genomic alignment and variant files in Python with pysam (htslib bindings) — SAM/BAM/CRAM alignments, VCF/BCF variants, and FASTA/FASTQ sequences, plus region extraction and per-base coverage/pileup. Use when scripting NGS data-processing pipelines that parse, filter, index, or compute coverage over BAM/CRAM/VCF files. Part of the AlterLab Academic Skills suite. license: MIT allowed-tools: Read Write Edit Bash(python:*) Bash(uv:*) compatibility: "Self-contained — runs under `uv run python` with the skill's Python package installed; no API key or account required. Written for pysam 0.24.x (current 0.24.1 as of 2026-09), which wraps htslib/samtools/bcftools 1.24 and supports Python 3.9-3.15. Wheels bundle htslib, so no separate samtools install is needed." metadata: skill-author: AlterLab version: "1.1.0" last_updated: "2026-09-23" --- # Pysam ## Overview Pysam is a Python module for reading, manipulating, and writing genomic datasets. Read/write SAM/BAM/CRAM alignment files, VCF/BCF variant files, and FASTA/FASTQ sequences with a Pythonic interface to htslib. Query tabix-indexed files, perform pileup analysis for coverage, and execute samtools/bcftools commands. ## When to Use This Skill This skill should be used when: - Working with sequencing alignment files (BAM/CRAM) - Analyzing genetic variants (VCF/BCF) - Extracting reference sequences or gene regions - Processing raw sequencing data (FASTQ) - Calculating coverage or read depth - Implementing bioinformatics analysis pipelines - Quality control of sequencing data - Variant calling and annotation workflows ### Does NOT Trigger | Scenario | Use Instead | |----------|-------------| | Running a whole variant-calling pipeline (alignment -> GATK/DeepVariant -> annotation) | `alterlab-nf-core-sarek` | | Storing and querying many samples' variants as a queryable array | `alterlab-tiledbvcf` | | Coverage tracks, bigWig generation, and deepTools-style BAM summaries | `alterlab-deeptools` | | Sequence-record parsing, translation, alignment objects (no BAM/VCF) | `alterlab-biopython` | | Transcript-level quantification from RNA-seq FASTQ | `alterlab-rnaseq-quant` | ## Quick Start ### Installation ```bash uv pip install pysam ``` ### Basic Examples **Read alignment file:** ```python import pysam # Open BAM file and fetch reads in region samfile = pysam.AlignmentFile("example.bam", "rb") for read in samfile.fetch("chr1", 1000, 2000): print(f"{read.query_name}: {read.reference_start}") samfile.close() ``` **Read variant file:** ```python # Open VCF file and iterate variants vcf = pysam.VariantFile("variants.vcf") for variant in vcf: print(f"{variant.chrom}:{variant.pos} {variant.ref}>{variant.alts}") vcf.close() ``` **Query reference sequence:** ```python # Open FASTA and extract sequence fasta = pysam.FastaFile("reference.fasta") sequence = fasta.fetch("chr1", 1000, 2000) print(sequence) fasta.close() ``` ## Core Capabilities ### 1. Alignment File Operations (SAM/BAM/CRAM) Use the `AlignmentFile` class to work with aligned sequencing reads. This is appropriate for analyzing mapping results, calculating coverage, extracting reads, or quality control. **Common operations:** - Open and read BAM/SAM/CRAM files - Fetch reads from specific genomic regions - Filter reads by mapping quality, flags, or other criteria - Write filtered or modified alignments - Calculate coverage statistics - Perform pileup analysis (base-by-base coverage) - Access read sequences, quality scores, and alignment information **Reference:** See `references/alignment_files.md` for detailed documentation on: - Opening and reading alignment files - AlignedSegment attributes and methods - Region-based fetching with `fetch()` - Pileup analysis for coverage - Writing and creating BAM files - Coordinate systems and indexing - Performance optimization tips ### 2. Variant File Operations (VCF/BCF) Use the `VariantFile` class to work with genetic variants from variant calling pipelines. This is appropriate for variant analysis, filtering, annotation, or population genetics. **Common operations:** - Read and write VCF/BCF files - Query variants in specific regions - Access variant information (position, alleles, quality) - Extract genotype data for samples - Filter variants by quality, allele frequency, or other criteria - Annotate variants with additional information - Subset samples or regions **Reference:** See `references/variant_files.md` for detailed documentation on: - Opening and reading variant files - VariantRecord attributes and methods - Accessing INFO and FORMAT fields - Working with genotypes and samples - Creating and writing VCF files - Filtering and subsetting variants - Multi-sample VCF operations ### 3. Sequence File Operations (FASTA/FASTQ) Use `FastaFile` for random access to reference sequences and `FastxFile` for reading raw sequencing data. This is appropriate for extracting gene sequences, validating variants against reference, or processing raw reads. **Common operations:** - Query reference sequences by genomic coordinates - Extract sequences for genes or regions of interest - Read FASTQ files with quality scores - Validate variant reference alleles - Calculate sequence statistics - Filter reads by quality or length - Convert between FASTA and FASTQ formats **Reference:** See `references/sequence_files.md` for detailed documentation on: - FASTA file access and indexing - Extracting sequences by region - Handling reverse complement for genes - Reading FASTQ files sequentially - Quality score conversion and filtering - Working with tabix-indexed files (BED, GTF, GFF) - Common sequence processing patterns ### 4. Integrated Bioinformatics Workflows Pysam excels at integrating multiple file types for comprehensive genomic analyses. Common workflows combine alignment files, variant files, and reference sequences. **Common workflows:** - Calculate coverage statistics for specific regions - Validate variants against aligned reads - Annotate variants with coverage information - Extract sequences around variant positions - Filter alignments or variants based on multiple criteria - Generate coverage tracks for visualization - Quality control across multiple data types **Reference:** See `references/common_workflows.md` for detailed examples of: - Quality control workflows (BAM statistics, reference consistency) - Coverage analysis (per-base coverage, low coverage detection) - Variant analysis (annotation, filtering by read support) - Sequence extraction (variant contexts, gene sequences) - Read filtering and subsetting - Integration patterns (BAM+VCF, VCF+BED, etc.) - Performance optimization for complex workflows ## Key Concepts ### Coordinate Systems **Critical:** Pysam uses **0-based, half-open** coordinates (Python convention): - Start positions are 0-based (first base is position 0) - End positions are exclusive (not included in the range) - Region 1000-2000 includes bases 1000-1999 (1000 bases total) **Exception:** Region strings in `fetch()` follow samtools convention (1-based): ```python samfile.fetch("chr1", 999, 2000) # 0-based: positions 999-1999 samfile.fetch("chr1:1000-2000") # 1-based string: positions 1000-2000 ``` **VCF files:** Use 1-based coordinates in the file format, but `VariantRecord.start` is 0-based. ### Indexing Requirements Random access to specific genomic regions requires index files: - **BAM files**: Require `.bai` index (create with `pysam.index()`) - **CRAM files**: Require `.crai` index - **FASTA files**: Require `.fai` index (create with `pysam.faidx()`) - **VCF.gz files**: Require `.tbi` tabix index (create with `pysam.tabix_index()`) - **BCF files**: Require `.csi` index Without an index, use `fetch(until_eof=True)` for sequential reading. ### File Modes Specify format when opening files: - `"rb"` - Read BAM (binary) - `"r"` - Read SAM (text) - `"rc"` - Read CRAM - `"wb"` - Write BAM - `"w"` - Write SAM - `"wc"` - Write CRAM ### CRAM: two behaviour changes since pysam 0.24 / htslib 1.22 These bite quietly, so check them before blaming your data. 1. **The default output CRAM version is now 3.1, not 3.0.** Files written with `"wc"` may not be readable by older samtools or by downstream tools pinned to an older htslib. Write 3.0 explicitly when the consumer is out of your control: ```python out = pysam.AlignmentFile("out.cram", "wc", header=src.header, reference_filename="ref.fa", format_options=["version=3.0"]) ``` `format_options` takes a list of `str` (it accepted `bytes` in older pysam). 2. **CRAM reference sequences are no longer fetched from EBI automatically.** CRAM stores reads relative to a reference, so reading one without the matching FASTA now fails instead of silently downloading it. Pass `reference_filename=` when opening, or set the `REF_PATH` / `REF_CACHE` environment variables to a local cache. ### Performance Considerations 1. **Always use indexed files** for random access operations 2. **Use `pileup()` for column-wise analysis** instead of repeated fetch operations 3. **Use `count()` for counting** instead of iterating and counting manually 4. **Process regions in parallel** when analyzing independent genomic regions 5. **Close files explicitly** to free resources 6. **Use `until_eof=True`** for sequential processing without index 7. **Avoid multiple iterators** unless necessary (use `multiple_iterators=True` if needed) ## Common Pitfalls 1. **Coordinate confusion:** Remember 0-based vs 1-based systems in different contexts 2. **Missing indices:** Many operations require index files—create them first 3. **Partial overlaps:** `fetch()` returns reads overlapping region boundaries, not just those fully contained 4. **Unbounded pileup:** `pileup(chrom, start, stop)` yields columns for every position spanned by overlapping reads, not just `start..stop`—pass `truncate=True` to restrict to the requested window 5. **Silent base-quality drop:** `count_coverage()` defaults to `quality_threshold=15`, so low-quality bases are omitted; set `quality_threshold=0` to count all 6. **Iterator scope:** Keep pileup iterator references alive to avoid "PileupProxy accessed after iterator finished" errors 7. **Quality score editing:** Cannot modify `query_qualities` in place after changing `query_sequence`—create a copy first 8. **Stream limitations:** Only stdin/stdout are supported for streaming, not arbitrary Python file objects 9. **Thread safety:** While GIL is released during I/O, comprehensive thread-safety hasn't been fully validated 10. **CRAM without a reference:** see the CRAM note above—`reference_filename=` or `REF_PATH` is now required ## Command-Line Tools Pysam provides access to samtools and bcftools commands: ```python # Sort BAM file pysam.samtools.sort("-o", "sorted.bam", "input.bam") # Index BAM pysam.samtools.index("sorted.bam") # View specific region pysam.samtools.view("-b", "-o", "region.bam", "input.bam", "chr1:1000-2000") # BCF tools pysam.bcftools.view("-O", "z", "-o", "output.vcf.gz", "input.vcf") ``` **Error handling:** ```python try: pysam.samtools.sort("-o", "output.bam", "input.bam") except pysam.SamtoolsError as e: print(f"Error: {e}") ``` ## Resources ### references/ Detailed documentation for each major capability: - **alignment_files.md** - Complete guide to SAM/BAM/CRAM operations, including AlignmentFile class, AlignedSegment attributes, fetch operations, pileup analysis, and writing alignments - **variant_files.md** - Complete guide to VCF/BCF operations, including VariantFile class, VariantRecord attributes, genotype handling, INFO/FORMAT fields, and multi-sample operations - **sequence_files.md** - Complete guide to FASTA/FASTQ operations, including FastaFile and FastxFile classes, sequence extraction, quality score handling, and tabix-indexed file access - **common_workflows.md** - Practical examples of integrated bioinformatics workflows combining multiple file types, including quality control, coverage analysis, variant validation, and sequence extraction ## Getting Help For detailed information on specific operations, refer to the appropriate reference document: - Working with BAM files or calculating coverage → `alignment_files.md` - Analyzing variants or genotypes → `variant_files.md` - Extracting sequences or processing FASTQ → `sequence_files.md` - Complex workflows integrating multiple file types → `common_workflows.md` Official documentation: https://pysam.readthedocs.io/
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.