Validate alignment quality with insert size distribution, proper pairing rates, GC bias, strand balance, and other post-alignment metrics. Use when verifying alignment data quality before variant calling or quantification.
Validate alignment quality with insert size distribution, proper pairing rates, GC bias, strand balance, and other post-alignment metrics. Use when verifying alignment data quality before variant calling or quantification.
Before using code patterns, verify installed versions match. If versions differ:
Python: pip show <package> then help(module.function) to check signatures
CLI: <tool> --version then <tool> --help to confirm flags
If code throws ImportError, AttributeError, or TypeError, introspect the installed
package and adapt the example to match the actual API rather than retrying.
Alignment Validation
Post-alignment quality control to verify alignment quality and identify issues.
A file can pass quickcheck and still be malformed in ways that crash GATK three hours into HaplotypeCaller. Conversely, a QC-poor BAM can be structurally valid.
If M5s differ, the BAM was aligned to a different sequence than the current reference (even if contig names match). Concrete failure modes: GRCh38 vs GRCh38.p13 vs GRCh38_no_alt (alt contigs differ); UCSC chr1 vs Ensembl 1 (names differ, M5s match -- pure renaming); soft-masked vs unmasked (M5 matches -- case is normalized to uppercase before hashing, so lowercase soft-masking is invisible to M5). Note hard-masking is different: it replaces bases with N, changing the sequence, so its M5 does NOT match the unmasked reference. The M5 tag is the only definitive identity check.
Contamination and Sample Swap
No alignment QC is complete without these in production:
# Cross-sample contamination
verifybamid2 --SVDPrefix /resources/1000g.b38.vcf.gz.SVD \
--Reference ref.fa --BamFile sample.bam --Output sample.contam
# FREEMIX > 0.03 is the commonly used contamination-concern threshold; values escalate from there.# Relatedness, sex check, sample swap detection
somalier extract -d extracted/ -s /resources/sites.GRCh38.vcf.gz \
-f ref.fa sample.bam
somalier relate --infer extracted/*.somalier
# Tumor/normal pairing verification
picard CrosscheckFingerprints I=tumor.bam I=normal.bam \
HAPLOTYPE_MAP=Homo_sapiens_assembly38.haplotype_database.txt
# LOD > 5 = same individual; < -5 = different
Sample-swap rates of 0.5-1% in production cohorts are typical. Without somalier or CrosscheckFingerprints, swaps are detected only when a downstream finding contradicts clinical expectation.
Insert Size Distribution
Goal: Verify that the fragment length distribution matches the library preparation protocol.
Approach: Extract template_length from properly paired reads and compare the distribution to expected values for the library type.
Sharpest, most symmetric peak (no PCR bias); bimodality = degraded sample
TruSeq DNA Nano (PCR) WGS
300-400 bp
Gaussian, broadened by PCR amplification bias
Twist / IDT exome capture
250-350 bp
Gaussian
TruSeq Stranded mRNA
200-300 bp
Right-skewed (transcript distribution)
Long tail = poor size selection
Ribo-Zero rRNA-depleted
250-400 bp
Right-skewed
Smart-seq2 / Smart-seq3
200-700 bp
Broad
10x Chromium (3')
n/a -- not informative
n/a
TruSeq ChIP
200-400 bp
Sharp
ATAC-seq (Buenrostro / Omni-ATAC)
Multimodal
Peaks at ~50, ~180, ~370 bp
Missing multimodal pattern = bad library; missing ~180 bp mononucleosome peak = over-transposition (Tn5 over-titrated) or degraded DNA
Hi-C / Micro-C
Multimodal
Peak at ligation-junction size
cfDNA / ctDNA
160-180 bp
Multimodal; ~167 bp mononucleosomal + ~340 dinuc
Tumor-derived shorter (~145 bp); shape itself is a biomarker
FFPE
100-250 bp
Right-skewed, broad
aDNA
30-80 bp
Sharp left-skewed
ONT (native)
1-30 kb
n/a
PacBio HiFi
10-25 kb
Sharp peak
For ATAC, the multimodal pattern is the QC. If the mononucleosomal peak (~180 bp) is absent, Tn5 was over-titrated, under-titrated, or DNA was degraded. Use ATACseqQC fragSizeDist() for the standard ATAC fragment-size diagnostic.
Python Insert Size Analysis
import pysam
import numpy as np
import matplotlib.pyplot as plt
defget_insert_sizes(bam_file, max_reads=100000):
sizes = []
bam = pysam.AlignmentFile(bam_file, 'rb')
for i, read inenumerate(bam.fetch()):
if i >= max_reads:
breakif read.is_proper_pair andnot read.is_secondary and read.template_length > 0:
sizes.append(read.template_length)
bam.close()
return sizes
sizes = get_insert_sizes('sample.bam')
print(f'Median insert size: {np.median(sizes):.0f}')
print(f'Mean insert size: {np.mean(sizes):.0f}')
print(f'Std dev: {np.std(sizes):.0f}')
plt.hist(sizes, bins=100, range=(0, 1000))
plt.xlabel('Insert Size')
plt.ylabel('Count')
plt.savefig('insert_size_dist.pdf')
A balanced 0.48-0.52 forward/reverse ratio applies to WGS / WES / generic DNA-seq on autosomes. Expected to deviate for: stranded RNA-seq (deliberately strand-asymmetric -- verify with RSeQC infer_experiment.py), bisulfite (CT vs GA), small-RNA / strand-specific RNA-seq, and chrY/chrM regions. Per-chromosome strand imbalance >5% on autosomes is a field-convention rule of thumb (no single primary citation) — it picks up aligner artifacts; on chrX/chrY it suggests sex-mismatch.
Mean MAPQ is misleading; distributions are bimodal (0 and aligner-max). For aligner-specific scales and "unique mapping" sentinels, see sam-bam-basics. The fraction of primary mapped reads at MAPQ >= 30 is a more informative summary than the mean.
samtools idxstats in.bam | awk '$2>0 && $1!~/^chr[XYM]|^GL|^KI|^chrUn|^chrEBV/ {
cov[$1] = $3 / $2
}
END {
n = asort(cov, sorted)
med = sorted[int(n/2)+1]
for (c in cov) printf "%s\t%.3f\n", c, cov[c]/med
}'
For full ancestry / contamination / relatedness checking, use verifybamid2, somalier, or peddy -- they account for population AFs, not just per-contig depth.
Goal: Run all key alignment QC checks in a single pass and generate a summary report.
Approach: Combine samtools flagstat, stats, idxstats, and strand counts into one script that outputs pass/warn/fail calls.
#!/bin/bash
BAM=$1
REF=$2
NAME=$(basename$BAM .bam)
OUTDIR=${3:-qc}mkdir -p $OUTDIRecho"=== Alignment Validation: $NAME ===" | tee$OUTDIR/report.txt
echo -e "\n--- Flagstat ---" | tee -a $OUTDIR/report.txt
samtools flagstat $BAM | tee -a $OUTDIR/report.txt
echo -e "\n--- Mapping Rate ---" | tee -a $OUTDIR/report.txt
mapped=$(samtools view -c -F 4 $BAM)
total=$(samtools view -c $BAM)
rate=$(echo"scale=2; $mapped / $total * 100" | bc)
echo"Mapping rate: ${rate}%" | tee -a $OUTDIR/report.txt
echo -e "\n--- Proper Pairing ---" | tee -a $OUTDIR/report.txt
proper=$(samtools view -c -f 2 $BAM)
pair_rate=$(echo"scale=2; $proper / $mapped * 100" | bc)
echo"Proper pairing: ${pair_rate}%" | tee -a $OUTDIR/report.txt
echo -e "\n--- Insert Size ---" | tee -a $OUTDIR/report.txt
samtools stats $BAM | grep "insert size average" | tee -a $OUTDIR/report.txt
echo -e "\n--- Strand Balance ---" | tee -a $OUTDIR/report.txt
fwd=$(samtools view -c -F 16 $BAM)
rev=$(samtools view -c -f 16 $BAM)
strand_ratio=$(echo"scale=3; $fwd / $rev" | bc)
echo"Forward: $fwd, Reverse: $rev, Ratio: $strand_ratio" | tee -a $OUTDIR/report.txt
echo -e "\n--- Chromosome Coverage ---" | tee -a $OUTDIR/report.txt
samtools idxstats $BAM | head -25 | tee -a $OUTDIR/report.txt
echo -e "\nReport: $OUTDIR/report.txt"
Python Validation Module
A skeleton; full implementation is in examples/validate_alignment.py:
import pysam
classAlignmentValidator:
def__init__(self, bam_file):
self.bam = pysam.AlignmentFile(bam_file, 'rb')
defreport(self, sample_size=100000):
# Sample reads, compute mapping rate, proper-pair rate, MAPQ dist, strand balance# See examples/validate_alignment.py for full implementation
...
The first-N-reads sampling pattern is biased toward chr1 (different GC content and complexity than chrM/chrX/chrY/alt contigs). For unbiased per-chromosome statistics, use samtools view -s 42.01 input.bam (the INT.FRAC form uses INT as the seed; specify an explicit nonzero seed so the subsample is documented and consistent across paired runs) instead of head-of-file iteration.
Quality Thresholds Summary
Metric
Good
Warning
Fail
Mapping rate
> 95%
90-95%
< 90%
Proper pairing
> 90%
80-90%
< 80%
Duplicate rate (assay-specific)
see bam-statistics decision table
--
--
Strand balance
0.48-0.52
0.45-0.55
Outside
Mean MAPQ
> 40
30-40
< 30
GC bias
< 1.2x
1.2-1.5x
> 1.5x (field-convention bands; Picard CollectGcBiasMetrics does not prescribe specific cutoffs)