Quality Control Best Practices
Last Updated: November 29, 2025
Quality control (QC) is critical at every stage of virome analysis. Poor QC leads to false positives, missed discoveries, and irreproducible results. This guide covers best practices for QC throughout the entire workflow.
Overview
QC checkpoints in virome analysis:
- Sample Quality - Before sequencing
- Sequencing Quality - Raw reads
- Assembly Quality - Contigs
- Viral Identification Quality - Predicted viral sequences
- Annotation Quality - Gene predictions and functional assignments
- Final Dataset Quality - Publication-ready results
1. Sample Quality Control
Pre-Sequencing QC
DNA/RNA Quantity:
# Qubit fluorometry (most accurate for low concentrations)
# Target: >50 ng total for standard library prep
# Minimum: 10 ng (may require amplification)
# Check concentration
qubit dsDNA HS sample.dna
# Expected: 5-100 ng/μL for virome samples
DNA/RNA Quality:
# Check purity ratios
nanodrop sample.dna
# Good quality indicators:
# 260/280 ratio: 1.8-2.0 (1.8 for DNA, 2.0 for RNA)
# 260/230 ratio: 2.0-2.2 (lower indicates contamination)
Contamination Indicators
- 260/280 < 1.7: Protein contamination
- 260/280 > 2.2: RNA contamination (DNA samples)
- 260/230 < 1.8: Salt, phenol, or humic acid contamination
- Brown color: Humic acids (soil samples) - requires additional cleanup
Fragment Size:
# Run Bioanalyzer or TapeStation
# Expected for viral samples:
# - DNA viruses: Broad range (few kb to 100+ kb)
# - High-quality samples: Distinct peaks >10 kb
# - Degraded samples: Smear <5 kb (may still be usable)
Negative Controls
Include at every stage: - Extraction blank: No sample input - Library prep blank: No DNA input - PCR blank: No template (if using amplification)
# Sequence negative controls
# Good run indicators:
# - <1% reads in neg controls vs samples
# - Different taxonomic composition than samples
# - No "sample-specific" viruses in neg controls
2. Sequencing Quality Control
Raw Read QC
Run FastQC or FastP:
# FastQC for visualization
fastqc reads_R1.fastq.gz reads_R2.fastq.gz -o qc_reports/
# FastP for QC + filtering
fastp \
-i reads_R1.fastq.gz -I reads_R2.fastq.gz \
-o clean_R1.fastq.gz -O clean_R2.fastq.gz \
-h report.html -j report.json \
--detect_adapter_for_pe \
--qualified_quality_phred 20 \
--length_required 50
Key Metrics to Check:
| Metric | Good | Warning | Poor |
|---|---|---|---|
| Total Reads | >10M | 5-10M | <5M |
| Q30 Score | >90% | 80-90% | <80% |
| GC Content | 35-55% | 30-60% | <30% or >60% |
| Adapter Content | <5% | 5-10% | >10% |
| Duplication Rate | <30% | 30-50% | >50%* |
*High duplication (30-70%) is normal for viral samples due to high viral abundance
Virome-Specific QC Expectations
- GC content: Highly variable (some viruses are AT-rich, others GC-rich)
- Duplication: Higher than bacterial metagenomes (20-50% normal)
- Insert size: Should match library prep protocol
Post-QC Metrics
# Count reads before/after QC
echo "Before QC: $(zcat raw_R1.fastq.gz | wc -l | awk '{print $1/4}')"
echo "After QC: $(zcat clean_R1.fastq.gz | wc -l | awk '{print $1/4}')"
# Acceptable QC pass rate:
# >85%: Excellent
# 70-85%: Good
# <70%: Poor - check sample quality or sequencing issues
3. Assembly Quality Control
Assembly Metrics
# Calculate N50, L50, total length
seqkit stats contigs.fasta
# Use MetaQUAST for detailed assembly metrics
metaquast.py contigs.fasta -o quast_results/
Key Assembly Metrics:
| Metric | Good | Acceptable | Poor |
|---|---|---|---|
| N50 | >10 kb | 5-10 kb | <5 kb |
| Longest Contig | >50 kb | 20-50 kb | <20 kb |
| Total Assembly | >10 Mb | 5-10 Mb | <5 Mb |
| Contigs >1kb | >1,000 | 500-1,000 | <500 |
Virome-specific considerations: - Small genomes expected (most 20-150 kb) - Fragmentation is common (incomplete genomes) - Chimeric contigs possible (mix of viruses)
Detect Chimeric Contigs
# Check for abrupt GC content changes
# Chimeric contigs show discontinuities in GC content
python3 << 'EOF'
from Bio import SeqIO
import numpy as np
def calculate_gc_sliding(seq, window=1000, step=500):
gc_values = []
for i in range(0, len(seq) - window, step):
subseq = seq[i:i+window]
gc = (subseq.count('G') + subseq.count('C')) / len(subseq) * 100
gc_values.append(gc)
return gc_values
for record in SeqIO.parse('contigs.fasta', 'fasta'):
if len(record.seq) > 5000: # Only check long contigs
gc_profile = calculate_gc_sliding(str(record.seq))
# Check for large GC jumps (>15% change = potential chimera)
if len(gc_profile) > 2:
max_jump = max(abs(gc_profile[i+1] - gc_profile[i])
for i in range(len(gc_profile)-1))
if max_jump > 15:
print(f"Potential chimera: {record.id} (GC jump: {max_jump:.1f}%)")
EOF
Coverage Analysis
# Map reads back to contigs to check coverage
bbmap.sh in=reads.fq ref=contigs.fasta out=mapped.sam covstats=coverage.txt
# Good assembly indicators:
# - Even coverage across contigs (no sudden drops)
# - Coverage >10x for most contigs
# - Consistent coverage within contigs (no breakpoints)
4. Viral Identification Quality Control
Multiple Tool Validation
Critical: Use Multiple Tools
Never rely on a single viral identification tool. False positive rates are 10-30% for individual tools.
# Run at least 2-3 tools:
# VirSorter2
virsorter run -i contigs.fa --min-score 0.5 -w vs2_out/ all
# VIBRANT
VIBRANT_run.py -i contigs.fa -folder vibrant_out/
# geNomad
genomad end-to-end contigs.fa genomad_out/ genomad_db/
# Take consensus: contigs predicted by ≥2 tools
# (See Tutorial 1 for detailed implementation)
Recommended Thresholds:
| Tool | Parameter | Conservative | Balanced | Sensitive |
|---|---|---|---|---|
| VirSorter2 | --min-score | 0.9 | 0.7 | 0.5 |
| VIBRANT | - | Default | Default | Default |
| geNomad | --min-score | 0.9 | 0.8 | 0.7 |
| Consensus | Min tools | 3/3 | 2/3 | 1/3* |
*Not recommended - high false positive rate
CheckV Quality Assessment
# Always run CheckV on viral predictions
checkv end_to_end viral_contigs.fasta checkv_out/ -t 8
# Filter by quality
awk -F'\t' '$8 == "Complete" || $8 == "High-quality" {print $1}' \
checkv_out/quality_summary.tsv > hq_viral_ids.txt
# Quality tier guidelines:
# Complete (>90% complete): Use for all analyses
# High-quality (>50% complete): Use for most analyses
# Medium-quality: Use for diversity, but validate taxonomy
# Low-quality: Use with extreme caution or exclude
CheckV Red Flags:
# Check for problems
awk -F'\t' '$10 > 10 {print $1, $10}' checkv_out/quality_summary.tsv
# High contamination (>10%) indicates:
# - False positive (not actually viral)
# - Prophage with flanking host genes
# - Chimeric assembly
# Solutions:
# - Use CheckV's cleaned sequences (contamination removed)
# - Increase viral prediction stringency
# - Manually inspect suspicious contigs
5. Annotation Quality Control
Gene Prediction QC
# Run Prodigal
prodigal -i viral_contigs.fa -a proteins.faa -p meta
# Check gene prediction sanity
python3 << 'EOF'
from Bio import SeqIO
proteins = list(SeqIO.parse('proteins.faa', 'fasta'))
total_prots = len(proteins)
# Calculate statistics
lengths = [len(p.seq) for p in proteins]
avg_length = sum(lengths) / len(lengths)
print(f"Total proteins predicted: {total_prots}")
print(f"Average protein length: {avg_length:.0f} aa")
# Red flags:
if avg_length < 100:
print("WARNING: Very short average protein length - check gene prediction")
if total_prots < 100:
print("WARNING: Very few proteins - check input")
EOF
# Expected for viral genomes:
# Average protein length: 150-300 aa
# Genes per kb: ~1-1.5 (viruses are gene-dense)
Functional Annotation QC
# After annotation (e.g., with Pharokka, DRAMv)
# Check for viral hallmark genes
# Expected in most viral genomes:
# - Terminase (DNA packaging)
# - Major capsid protein
# - Portal protein
# - Tail proteins (tailed phages)
# Red flags:
# - >50% hypothetical proteins (normal for novel viruses)
# - Many bacterial genes (possible host contamination)
# - No structural genes (possible false positive)
6. Taxonomic Assignment Quality Control
BLAST-Based QC
# Check BLAST hits for sanity
blastn -query viral_contigs.fa -db nt -outfmt 6 -max_target_seqs 5 \
> blast_results.txt
# Analyze top hits
cut -f1,2,3 blast_results.txt | head -20
# Red flags:
# - Top hits to non-viral sequences (bacteria, eukaryotes)
# - Very low identity to known viruses (<70%)
# - Hits to vectors, contaminants
Identity Interpretation:
| BLAST Identity | Interpretation | Confidence |
|---|---|---|
| >95% | Same species/strain | High |
| 85-95% | Same genus | High |
| 70-85% | Same family | Medium |
| 50-70% | Distantly related | Low |
| <50% | Very distant or novel | Very Low |
Taxonomic Consistency Check
# Check that taxonomic assignments are consistent
# E.g., if proteins BLAST to different virus families, investigate
python3 << 'EOF'
import pandas as pd
# Load protein BLAST results
blast = pd.read_csv('protein_blast.txt', sep='\t',
header=None, names=['protein', 'hit', 'pident', ...])
# Extract virus families (requires parsing taxonomy)
# Check if all proteins from same contig hit same family
# Red flag: Proteins from same contig hit different families
# Could indicate:
# - Chimeric contig
# - Horizontal gene transfer
# - Incorrect taxonomy in database
EOF
7. Abundance Estimation Quality Control
Mapping QC
# Check mapping statistics
samtools flagstat mapped.bam
# Good mapping indicators:
# - Properly paired: >80%
# - Mapping quality >30: >90%
# - Duplicates: <50% (higher for abundant viruses OK)
# Coverage distribution
samtools depth mapped.bam | awk '{sum+=$3; count++} END {print "Mean coverage:", sum/count}'
Coverage Uniformity
# Check for even coverage (no bias)
# Uneven coverage indicates:
# - PCR bias
# - Repetitive regions
# - Chimeric contigs
# Calculate coefficient of variation (CV) for coverage
python3 << 'EOF'
import pandas as pd
import numpy as np
depth = pd.read_csv('coverage.txt', sep='\t',
header=None, names=['contig', 'pos', 'depth'])
# Calculate CV per contig
for contig in depth['contig'].unique():
contig_depth = depth[depth['contig'] == contig]['depth']
mean_cov = contig_depth.mean()
std_cov = contig_depth.std()
cv = (std_cov / mean_cov) * 100 if mean_cov > 0 else 0
if cv > 100: # Very high CV
print(f"WARNING: Uneven coverage for {contig} (CV={cv:.1f}%)")
EOF
8. Reproducibility QC
Document Everything
Create analysis log:
# Record all commands and versions
echo "Analysis started: $(date)" > analysis_log.txt
echo "VirSorter2 version: $(virsorter -v)" >> analysis_log.txt
echo "CheckV version: $(checkv -v)" >> analysis_log.txt
# ... etc.
# Log parameters
echo "VirSorter2 command:" >> analysis_log.txt
echo "virsorter run --min-score 0.5 ..." >> analysis_log.txt
Use version control:
# Track analysis scripts
git init virome_analysis/
git add *.sh *.py
git commit -m "Initial analysis scripts"
Random Seed Setting
# Set random seeds for reproducible results
# In Python:
import numpy as np
import random
np.random.seed(42)
random.seed(42)
# In R:
set.seed(42)
# For tools with random components (assembly, subsampling):
spades.py --seed 42 ...
seqtk sample -s42 reads.fq 1000000 > subsample.fq
9. Final Dataset QC Checklist
Before publication or downstream analysis:
- [ ] Sequencing depth adequate (>10M reads for diversity studies)
- [ ] Assembly quality acceptable (N50 >5kb)
- [ ] Viral predictions validated (consensus from ≥2 tools)
- [ ] CheckV quality assessed (majority High-quality or better)
- [ ] Contamination removed (host sequences, non-viral contigs)
- [ ] Negative controls clean (<1% of sample reads)
- [ ] Taxonomic assignments reasonable (consistent with expected environment)
- [ ] Abundance estimates valid (even coverage, good mapping rates)
- [ ] Analysis documented (versions, parameters, scripts saved)
- [ ] Results reproducible (random seeds set, workflows saved)
10. Common QC Failures and Solutions
Problem: High Contamination in CheckV
Diagnosis:
Solutions: - Use CheckV's cleaned sequences (contamination auto-removed) - Increase viral prediction stringency - Check if samples are actually virome-enriched (VLP prep) - Filter out contigs with >10% contamination
Problem: Most Viral Predictions are Low Quality
Diagnosis:
Solutions: - Increase sequencing depth (fragmentation due to low coverage) - Use longer read technology (PacBio, Nanopore) - Accept that novel viruses have lower completeness - Focus on High-quality+ subset for key analyses
Problem: Very Few Viral Sequences Identified
Possible causes: 1. Sample not enriched for viruses (need VLP prep) 2. Highly novel viruses (not recognized by tools) 3. Low viral biomass 4. Assembly fragmentation
Diagnostic:
# Check if viral reads present
seqtk sample -s42 reads.fq 100000 > subsample.fq
blastn -query subsample.fq -db viral_refseq -outfmt 6 | wc -l
# If many viral BLAST hits but few contigs:
# -> Assembly issue, try different assembler or parameters
# If few viral BLAST hits:
# -> Low viral content or highly novel viruses
Best Practices Summary
- QC at Every Step: Don't wait until the end to check quality
- Use Multiple Methods: Consensus predictions reduce false positives
- Keep Negative Controls: Identify and remove contamination
- Document Everything: Record versions, parameters, and decisions
- Validate Key Findings: Experimental validation for important results
- Be Realistic: Expect incomplete genomes and unknown taxonomy
- Set Thresholds Appropriately: Balance sensitivity vs specificity for your goals
Further Reading
- Roux, S., et al. (2019). "Minimum Information about an Uncultivated Virus Genome (MIUViG)." Nature Biotechnology, 37(1), 29-37.
- Nayfach, S., et al. (2021). "CheckV assesses the quality and completeness of metagenome-assembled viral genomes." Nature Biotechnology, 39(5), 578-585.
- Ewels, P., et al. (2016). "MultiQC: summarize analysis results for multiple tools and samples in a single report." Bioinformatics, 32(19), 3047-3048.