Variant Calling: From FASTQ to VCF

This is where the whole track pays off. You'll chain every skill you've built, QC, trimming, alignment, sorting, into one mini-pipeline that takes raw reads and produces a VCF: the precise list of places where your sample differs from the reference genome. That list of differences is the point of the entire workflow.

๐Ÿ† Capstone ๐ŸŸ  Intermediate+ โฑ๏ธ ~2 hr ๐ŸŒ Runs free in Colab

Before you start

Learning objectives

By the end of this lesson you will be able to: take raw sequencing reads through alignment to a list of variants (a VCF), mark duplicates, and judge a called variant by its depth and quality.

What "calling a variant" means

Every person's genome differs from the reference at millions of spots. A variant is one of those differences: a single letter changed (a SNP), or a few letters inserted or deleted (an indel). Variant calling is the process of reading the stacked-up reads at each position and deciding, statistically, "the reference says A here, but the reads clearly say G, so this sample carries a variant."

The skill is knowing the difference between a real variant and a sequencing error. One read disagreeing with the reference is probably noise. Twenty reads all agreeing on the same change is a variant. A variant caller does exactly this reasoning, position by position, across the whole genome.

Decode the jargonSNP and indel

A SNP (single-nucleotide polymorphism) is a one-letter difference, like an A where the reference has a G. An indel is a small insertion or deletion of bases. Together they're the most common kinds of variants, and the ones you'll call in this lesson.

The whole pipeline on one screen

FASTQraw reads โ†’ fastptrim โ†’ bwa memalign โ†’ samtoolssort + index โ†’ bcftoolscall โ†’ VCFvariants

You already know the first four boxes. This lesson adds the last one, bcftools, and connects them all into a single runnable pipeline.

โ–ถ Run the full pipeline in Google Colab

The notebook builds a reference, plants known variants in a sample, then recovers them, so you can check the answer.

Open the notebook in Colab โ†’

1Install and prepare

We need three tools: bwa to align, samtools to sort and index, and bcftools to call variants.

!apt-get -qq install -y bwa samtools bcftools

The notebook then creates a reference and a sample genome that's identical except for a few planted SNPs at positions we record. Reads are generated from the sample, so when we call variants we should recover exactly those planted changes, a built-in answer key.

2Align, sort, index

This is everything from the last two lessons, compressed into the standard three lines. Notice how each tool hands off to the next.

!bwa index reference.fasta
!bwa mem reference.fasta sample.fastq | samtools sort -o sample.sorted.bam
!samtools markdup sample.sorted.bam sample.markdup.bam   # flag PCR/optical duplicates
!samtools index sample.markdup.bam

Why mark duplicates?

PCR and optical duplicates are copies of the same original fragment. Left in, they inflate the apparent support for a position and cause false variant calls, so real pipelines flag them before calling. On paired-end data you run samtools fixmate -m (on a name-sorted BAM) before the coordinate sort so markdup has the mate information it needs.

3Call variants with bcftools

Calling is two steps piped together. bcftools mpileup walks the genome and, at every position, gathers the pile of read bases sitting there. bcftools call then judges each pile and decides whether it's a variant.

!bcftools mpileup -f reference.fasta sample.markdup.bam | \
  bcftools call -mv -Oz -o calls.vcf.gz
!bcftools norm -f reference.fasta calls.vcf.gz -Oz -o calls.norm.vcf.gz   # left-align indels
!bcftools index calls.norm.vcf.gz

The extra bcftools norm step left-aligns and normalises indels so the same insertion or deletion is always written the same way, which matters the moment you compare or annotate variants on real data.

The flags read as: -m use the modern multiallelic caller, -v output only variant sites (skip positions that match the reference), and -Oz write a compressed VCF.

Decode the jargonPileup

A pileup is the column of read bases stacked over a single genome position, like reading straight down through all the reads that cover that spot. The caller looks at each column and asks: do the reads agree with the reference, or do enough of them disagree to call a variant?

4Read your VCF

VCF (Variant Call Format) is the standard text file listing variants. After header lines (starting with #), each row is one variant.

!bcftools view calls.vcf.gz | grep -v '^##' | head
ColumnMeaning
CHROMWhich chromosome the variant is on.
POSThe position of the variant.
REFThe base in the reference genome.
ALTThe alternative base seen in your sample, the change.
QUALHow confident the caller is that this variant is real.
INFOExtra details, such as depth (DP) at this site.

Compare the POS and ALT columns to the variants the notebook planted. If they match, you've just validated your own pipeline end to end.

โš ๏ธ Not every call is real

Variant callers make mistakes, especially at low coverage or in repetitive regions. Real projects filter the VCF by quality and depth (for example, dropping calls with QUAL below a threshold or very low DP) before trusting it. Raw calls are a starting point, not a final answer.

๐Ÿ† Your Track 2 deliverable

Turn the notebook into something you can show. Build a documented variant-calling mini-pipeline:

  • Run the full FASTQ โ†’ VCF pipeline end to end in the notebook.
  • Add a markdown cell at the top explaining, in your own words, what each stage does and why.
  • Report two numbers: your mapping rate (from flagstat) and how many variants you called.
  • Confirm the called variants match the planted ones, and write one sentence on what would change with real data.

Save it to your GitHub and you have a portfolio piece that shows you can run the core genomics workflow.

๐Ÿ”ถ Level up: real data and GATK

For human germline work, the field standard is the GATK Best Practices pipeline, which adds duplicate marking, base-quality recalibration, and joint genotyping. bcftools (what you used here) is lighter, faster, and excellent for many projects, especially microbial genomes and quick analyses. Same concepts, more rigor. Try the pipeline next on a small real dataset, like an E. coli run from the ENA.

Now do it on real data: E. coli from the ENA

The toy run above is honest about being a rehearsal. Here is the same pipeline on a real sequencing run of E. coli, downloaded from the European Nucleotide Archive (ENA). Everything runs in Colab; the download and alignment take a few minutes, not seconds, because this is real data. The reads are a real isolate from the Lenski long-term evolution experiment (run SRR2584866), and the reference is the standard E. coli K-12 genome.

!apt-get -qq install -y bwa samtools bcftools fastp

# 1. Reference genome (E. coli K-12 MG1655)
!wget -q https://ftp.ncbi.nlm.nih.gov/genomes/all/GCF/000/005/845/GCF_000005845.2_ASM584v2/GCF_000005845.2_ASM584v2_genomic.fna.gz
!gunzip -f GCF_000005845.2_ASM584v2_genomic.fna.gz && mv GCF_000005845.2_ASM584v2_genomic.fna ecoli.fasta

# 2. A real paired-end sequencing run from the ENA
!wget -q https://ftp.sra.ebi.ac.uk/vol1/fastq/SRR258/006/SRR2584866/SRR2584866_1.fastq.gz
!wget -q https://ftp.sra.ebi.ac.uk/vol1/fastq/SRR258/006/SRR2584866/SRR2584866_2.fastq.gz

# 3. QC and trim (paired-end: fastp auto-detects adapters from the read overlap)
!fastp -i SRR2584866_1.fastq.gz -I SRR2584866_2.fastq.gz -o r1.fq.gz -O r2.fq.gz -h fastp.html

# 4. Align, then fixmate -> sort -> markdup (the real paired-end dedup flow)
!bwa index ecoli.fasta
!bwa mem ecoli.fasta r1.fq.gz r2.fq.gz | samtools sort -n -o namesorted.bam
!samtools fixmate -m namesorted.bam fixmate.bam
!samtools sort -o sorted.bam fixmate.bam
!samtools markdup sorted.bam markdup.bam && samtools index markdup.bam

# 5. How well did the reads ACTUALLY map? (this number will not be 100%)
!samtools flagstat markdup.bam

# 6. Call and normalise
!bcftools mpileup -f ecoli.fasta markdup.bam | bcftools call -mv --ploidy 1 -Oz -o ecoli.vcf.gz
!bcftools norm -f ecoli.fasta ecoli.vcf.gz -Oz -o ecoli.norm.vcf.gz && bcftools index ecoli.norm.vcf.gz
!bcftools stats ecoli.norm.vcf.gz | grep "number of records:"

What real data breaks (that the toy run hid)

Look at flagstat: the mapping rate is high but not 100%, because real reads include contamination, low-quality ends, and repeats that map ambiguously. markdup now flags a real percentage of duplicates. There is no planted answer key; you judge each call by its depth (DP) and quality (QUAL) instead of checking it against a list. And you get hundreds of variants, some multi-allelic and some indels, which is exactly why the norm step matters. This is the gap between "ran the notebook" and "ran an analysis," and you just crossed it.

Check your understanding

What is the difference between a sequencing error and a variant?
Both look like a read disagreeing with the reference. The difference is consensus and consistency: a sequencing error appears in one or a few reads at random, while a true variant appears in many reads covering that position, consistently. The caller uses depth and quality to tell them apart.
What do bcftools mpileup and bcftools call each contribute?
mpileup gathers the evidence: at each position it assembles the pile of read bases and their qualities. call makes the decision: it judges each pile statistically and outputs the positions it believes are genuine variants.
Why filter a VCF instead of trusting every line?
Callers produce false positives, particularly where coverage is thin or the region is repetitive. Filtering by quality and depth removes the shakiest calls so that what remains is trustworthy. Skipping this step lets sequencing noise masquerade as biology.
In a VCF, what do the REF and ALT columns describe?
REF is the base the reference genome carries at that position, and ALT is the alternative base observed in your sample, the actual change. POS gives the location and QUAL the caller's confidence.
What is the difference between a SNP and an indel?
A SNP (single-nucleotide polymorphism) is a single-letter substitution, like an A where the reference has a G. An indel is a small insertion or deletion of bases. Both are common variant types you call in this lesson.
Track 2 complete ยท keep going

On to Track 3: Transcriptomics & RNA-seq โ†’