Skip to content

Tutorial 4: Comparative Virome Analysis

Last Updated: November 29, 2025 Level: Advanced | Time: 8-12 hours | Data Size: 500MB

Overview

Comparative virome analysis allows you to understand how viral communities differ across samples, conditions, time points, or environments. This tutorial covers the complete workflow for comparing viromes across multiple samples, including statistical analysis and biological interpretation.

What you'll learn: - Design considerations for comparative studies - Batch processing of multiple samples - Viral contig clustering and dereplication - Abundance normalization strategies - Statistical comparison of viral communities - Alpha and beta diversity analysis - Differential abundance testing - Network analysis and visualization

Sample dataset: Time-series marine virome (12 samples: 4 time points × 3 replicates, ~5M reads each)

Prerequisites

Required Software

# Create environment for comparative analysis
conda create -n comparative_virome python=3.9
conda activate comparative_virome

# Install analysis tools
conda install -c bioconda -c conda-forge \
    fastp=0.23.4 \
    spades=3.15.5 \
    virsorter=2.2.4 \
    checkv=1.0.1 \
    blast=2.14.0 \
    prodigal=2.6.3 \
    hmmer=3.3.2 \
    seqkit=2.5.1 \
    bbmap=39.01 \
    coverm=0.6.1 \
    cd-hit=4.8.1 \
    salmon=1.9.0 \
    python=3.9 \
    r-base=4.2 \
    bioconductor-deseq2 \
    bioconductor-phyloseq \
    r-vegan \
    r-ggplot2 \
    r-tidyverse \
    r-pheatmap

# Install vConTACT2 (for viral clustering)
pip install vcontact2

# Install InStrain (for strain-level analysis)
pip install instrain

System Requirements

  • RAM: 64GB minimum (128GB recommended for large datasets)
  • Disk Space: 200GB free
  • CPU: 16+ cores recommended
  • OS: Linux or macOS

Background Knowledge

Complete Tutorial 1 first. Familiarity with: - Statistical concepts (p-values, multiple testing correction) - Community ecology (alpha/beta diversity) - R programming basics

Step 1: Experimental Design and Data Organization

Download Dataset

# Create project directory
mkdir -p ~/comparative_virome
cd ~/comparative_virome

# Download time-series marine virome dataset
# 12 samples: T1, T2, T3, T4 (4 time points) × R1, R2, R3 (3 replicates)
mkdir -p 00_raw_data

# Simulated download (replace with actual Zenodo link)
for timepoint in T1 T2 T3 T4; do
    for replicate in R1 R2 R3; do
        sample="${timepoint}_${replicate}"
        echo "Downloading $sample..."
        # wget https://zenodo.org/record/EXAMPLE/files/${sample}_R1.fastq.gz
        # wget https://zenodo.org/record/EXAMPLE/files/${sample}_R2.fastq.gz
    done
done

Create Sample Metadata

# Create metadata file
cat > sample_metadata.tsv << 'EOF'
Sample  Timepoint   Replicate   Season  Depth_m Temp_C
T1_R1   T1  R1  Winter  10  8.5
T1_R2   T1  R2  Winter  10  8.3
T1_R3   T1  R3  Winter  10  8.7
T2_R1   T2  R1  Spring  10  12.1
T2_R2   T2  R2  Spring  10  12.3
T2_R3   T2  R3  Spring  10  11.9
T3_R1   T3  R1  Summer  10  18.4
T3_R2   T3  R2  Summer  10  18.1
T3_R3   T3  R3  Summer  10  18.6
T4_R1   T4  R1  Fall    10  14.2
T4_R2   T4  R2  Fall    10  14.5
T4_R3   T4  R3  Fall    10  14.1
EOF

cat sample_metadata.tsv

Metadata is Critical

Proper metadata is essential for comparative analysis. Include all relevant experimental factors (time, treatment, location, etc.) that might explain viral community variation.

Step 2: Batch Quality Control

Process all samples identically for fair comparison.

Run QC on All Samples

# Create QC directory
mkdir -p 01_qc

# Create sample list
ls 00_raw_data/*_R1.fastq.gz | sed 's/_R1.fastq.gz//' | xargs -n1 basename > sample_list.txt

# Batch QC with parallel processing
cat sample_list.txt | while read sample; do
    echo "QC for $sample..."

    fastp \
        -i 00_raw_data/${sample}_R1.fastq.gz \
        -I 00_raw_data/${sample}_R2.fastq.gz \
        -o 01_qc/${sample}_clean_R1.fastq.gz \
        -O 01_qc/${sample}_clean_R2.fastq.gz \
        -h 01_qc/${sample}_fastp.html \
        -j 01_qc/${sample}_fastp.json \
        --detect_adapter_for_pe \
        --correction \
        --cut_front \
        --cut_tail \
        --cut_window_size 4 \
        --cut_mean_quality 20 \
        --qualified_quality_phred 20 \
        --length_required 50 \
        --thread 4

done

# Summarize QC results
python3 << 'EOF'
import json
import pandas as pd

qc_summary = []

with open('sample_list.txt') as f:
    for line in f:
        sample = line.strip()
        json_file = f'01_qc/{sample}_fastp.json'

        try:
            with open(json_file) as jf:
                data = json.load(jf)

                qc_summary.append({
                    'Sample': sample,
                    'Total_Reads': data['summary']['before_filtering']['total_reads'],
                    'Reads_After_QC': data['summary']['after_filtering']['total_reads'],
                    'QC_Pass_Rate': data['summary']['after_filtering']['total_reads'] / data['summary']['before_filtering']['total_reads'] * 100,
                    'Q30_Rate': data['summary']['after_filtering']['q30_rate']
                })
        except:
            print(f"Warning: Could not parse {json_file}")

df = pd.DataFrame(qc_summary)
df.to_csv('01_qc/qc_summary.tsv', sep='\t', index=False)
print(df)
print(f"\nMean QC pass rate: {df['QC_Pass_Rate'].mean():.1f}%")
EOF

Expected QC results: - QC pass rate: 85-95% for all samples - Q30 rate: >90% - Consistent across samples (CV <10%)

Step 3: Co-Assembly Strategy

For comparative analysis, we can either: 1. Co-assemble all samples together (finds shared viruses) 2. Assemble each sample individually (finds sample-specific viruses) 3. Hybrid approach (recommended)

We'll use the hybrid approach.

3.1 Co-Assembly

# Create assembly directory
mkdir -p 02_assembly/co_assembly

# Concatenate all cleaned reads for co-assembly
cat 01_qc/*_clean_R1.fastq.gz > 02_assembly/co_assembly/all_R1.fastq.gz
cat 01_qc/*_clean_R2.fastq.gz > 02_assembly/co_assembly/all_R2.fastq.gz

# Run metaviralSPAdes on combined dataset
spades.py \
    --metaviral \
    -1 02_assembly/co_assembly/all_R1.fastq.gz \
    -2 02_assembly/co_assembly/all_R2.fastq.gz \
    -o 02_assembly/co_assembly/metaviralspades \
    -t 16 \
    -m 100

# Filter contigs ≥2kb (slightly higher than single sample)
seqkit seq -m 2000 02_assembly/co_assembly/metaviralspades/contigs.fasta \
    > 02_assembly/co_assembly_contigs_2kb.fasta

seqkit stats 02_assembly/co_assembly_contigs_2kb.fasta

Expected co-assembly: - Total contigs ≥2kb: 2,000-5,000 - N50: 8-20 kb - Longest contig: 100-300 kb

3.2 Individual Sample Assemblies (Optional)

# Assemble each sample individually (in parallel)
mkdir -p 02_assembly/individual

cat sample_list.txt | while read sample; do
    echo "Assembling $sample..."

    spades.py \
        --metaviral \
        -1 01_qc/${sample}_clean_R1.fastq.gz \
        -2 01_qc/${sample}_clean_R2.fastq.gz \
        -o 02_assembly/individual/${sample} \
        -t 4 \
        -m 16 &

    # Control parallel jobs (max 4 assemblies at once)
    if (( $(jobs -r | wc -l) >= 4 )); then
        wait -n
    fi
done

wait  # Wait for all assemblies to complete

# Filter and combine individual assemblies
cat 02_assembly/individual/*/contigs.fasta | \
    seqkit seq -m 2000 > 02_assembly/individual_contigs_2kb.fasta

3.3 Dereplicate Combined Contigs

# Combine co-assembly and individual assemblies
cat 02_assembly/co_assembly_contigs_2kb.fasta \
    02_assembly/individual_contigs_2kb.fasta \
    > 02_assembly/combined_all_contigs.fasta

# Dereplicate with CD-HIT-EST (95% identity, 90% coverage)
cd-hit-est \
    -i 02_assembly/combined_all_contigs.fasta \
    -o 02_assembly/dereplicated_contigs.fasta \
    -c 0.95 \
    -aS 0.90 \
    -M 32000 \
    -T 16

seqkit stats 02_assembly/dereplicated_contigs.fasta

Expected after dereplication: - Non-redundant contigs: 3,000-7,000 - Deduplication rate: ~30-50%

Step 4: Viral Identification

Run VirSorter2 on Dereplicated Contigs

# Create viral ID directory
mkdir -p 03_viral_id

# Run VirSorter2
virsorter run \
    -w 03_viral_id/virsorter2 \
    -i 02_assembly/dereplicated_contigs.fasta \
    --min-length 2000 \
    --min-score 0.5 \
    --include-groups dsDNAphage,ssDNA \
    -j 16 \
    all

# Extract viral contigs
cp 03_viral_id/virsorter2/final-viral-combined.fa 03_viral_id/viral_contigs.fasta

# Count
grep -c ">" 03_viral_id/viral_contigs.fasta

Quality Assessment with CheckV

# Create CheckV directory
mkdir -p 04_checkv

# Run CheckV
checkv end_to_end \
    03_viral_id/viral_contigs.fasta \
    04_checkv \
    -t 16 \
    -d /path/to/checkv-db-v1.5

# Filter for medium-quality and above
awk -F'\t' '$8 == "Complete" || $8 == "High-quality" || $8 == "Medium-quality" {print $1}' \
    04_checkv/quality_summary.tsv \
    > 04_checkv/good_quality_ids.txt

seqkit grep -f 04_checkv/good_quality_ids.txt \
    03_viral_id/viral_contigs.fasta \
    > 04_checkv/viral_contigs_hq.fasta

echo "High-quality viral contigs: $(grep -c ">" 04_checkv/viral_contigs_hq.fasta)"

Expected viral identification: - Viral contigs: 800-2,000 - High + Medium quality: 400-1,200

Step 5: Viral Contig Clustering (vOTUs)

Create viral Operational Taxonomic Units (vOTUs) - groups of similar viruses.

5.1 Cluster at 95% ANI

# Create vOTU directory
mkdir -p 05_vOTUs

# Cluster with CD-HIT-EST (95% identity = species-level)
cd-hit-est \
    -i 04_checkv/viral_contigs_hq.fasta \
    -o 05_vOTUs/vOTUs_95.fasta \
    -c 0.95 \
    -aS 0.85 \
    -M 32000 \
    -T 16

# Parse cluster file to create OTU table
python3 << 'EOF'
import pandas as pd

# Parse CD-HIT cluster file
clusters = {}
cluster_id = 0

with open('05_vOTUs/vOTUs_95.fasta.clstr') as f:
    for line in f:
        if line.startswith('>'):
            cluster_id = int(line.strip().split()[1])
        else:
            # Extract contig name
            contig = line.split('>')[1].split('...')[0]
            is_representative = '*' in line

            clusters[contig] = {
                'vOTU_ID': f'vOTU_{cluster_id}',
                'is_representative': is_representative
            }

# Save mapping
df = pd.DataFrame.from_dict(clusters, orient='index')
df.index.name = 'contig_id'
df.to_csv('05_vOTUs/contig_to_vOTU_mapping.tsv', sep='\t')

print(f"Total vOTUs (95% ANI): {len(df['vOTU_ID'].unique())}")
print(f"Total contigs: {len(df)}")
print(f"Mean contigs per vOTU: {len(df) / len(df['vOTU_ID'].unique()):.1f}")
EOF

Expected vOTU clustering: - vOTUs (95% ANI): 300-800 - Contigs per vOTU: 1-5 (mean ~1.5)

Step 6: Abundance Profiling Across All Samples

Map reads from each sample to the dereplicated viral contigs.

6.1 Read Mapping with CoverM

# Create abundance directory
mkdir -p 06_abundance

# Map all samples to viral contigs using CoverM (batch mode)
coverm contig \
    --coupled $(ls 01_qc/*_clean_R1.fastq.gz | tr '\n' ' ') \
    --reference 04_checkv/viral_contigs_hq.fasta \
    --methods mean trimmed_mean covered_fraction \
    --min-covered-fraction 0 \
    --min-read-percent-identity 95 \
    --threads 16 \
    --output-file 06_abundance/viral_coverage_all_samples.tsv

# Convert to vOTU-level abundance table
python3 << 'EOF'
import pandas as pd

# Load coverage data
coverage = pd.read_csv('06_abundance/viral_coverage_all_samples.tsv', sep='\t')

# Load vOTU mapping
votu_map = pd.read_csv('05_vOTUs/contig_to_vOTU_mapping.tsv', sep='\t')

# Merge coverage with vOTU assignments
merged = coverage.merge(votu_map, left_on='Contig', right_on='contig_id', how='left')

# Sum abundance for each vOTU (across member contigs)
# Group by vOTU and sample, sum coverage

# Reshape to wide format (vOTUs × samples)
# This is simplified - full implementation would aggregate across samples properly

# For tutorial, we'll use the contig-level table
# In practice, you'd aggregate to vOTU level here

print("Coverage matrix created")
# coverage.to_csv('06_abundance/vOTU_abundance_table.tsv', sep='\t', index=False)
EOF

6.2 Normalize Abundance

# Create normalized abundance table
python3 << 'EOF'
import pandas as pd
import numpy as np

# Load abundance table
# Assuming columns: Contig, Sample1_Mean, Sample2_Mean, ...
abundance = pd.read_csv('06_abundance/viral_coverage_all_samples.tsv', sep='\t')

# Extract sample columns (those ending with "Mean")
sample_cols = [col for col in abundance.columns if 'Mean' in col]

# Calculate relative abundance (normalize to proportions)
abundance_rel = abundance.copy()
for col in sample_cols:
    total = abundance[col].sum()
    if total > 0:
        abundance_rel[col] = abundance[col] / total * 100  # Convert to percentage

abundance_rel.to_csv('06_abundance/viral_abundance_relative.tsv', sep='\t', index=False)

# Calculate presence/absence (binary)
abundance_pa = abundance.copy()
for col in sample_cols:
    abundance_pa[col] = (abundance[col] > 0).astype(int)

abundance_pa.to_csv('06_abundance/viral_presence_absence.tsv', sep='\t', index=False)

print("Normalized abundance tables created")
EOF

Step 7: Statistical Analysis in R

7.1 Alpha Diversity

#!/usr/bin/env Rscript

library(vegan)
library(ggplot2)
library(tidyverse)

# Load abundance table
abundance <- read.delim('06_abundance/viral_coverage_all_samples.tsv', row.names=1)

# Transpose (samples as rows, vOTUs as columns)
abundance_t <- t(abundance)

# Calculate richness (number of vOTUs per sample)
richness <- rowSums(abundance_t > 0)

# Calculate Shannon diversity
shannon <- diversity(abundance_t, index='shannon')

# Calculate Simpson diversity
simpson <- diversity(abundance_t, index='simpson')

# Combine into data frame
alpha_diversity <- data.frame(
  Sample = rownames(abundance_t),
  Richness = richness,
  Shannon = shannon,
  Simpson = simpson
)

# Add metadata
metadata <- read.delim('sample_metadata.tsv')
alpha_diversity <- merge(alpha_diversity, metadata, by='Sample')

# Save
write.table(alpha_diversity, '07_statistics/alpha_diversity.tsv',
            sep='\t', row.names=FALSE, quote=FALSE)

# Plot richness by timepoint
pdf('07_statistics/alpha_diversity_richness.pdf', width=8, height=6)
ggplot(alpha_diversity, aes(x=Timepoint, y=Richness, fill=Timepoint)) +
  geom_boxplot() +
  geom_jitter(width=0.2) +
  labs(title='Viral Richness Across Timepoints',
       x='Timepoint', y='Number of vOTUs') +
  theme_minimal()
dev.off()

# Plot Shannon diversity
pdf('07_statistics/alpha_diversity_shannon.pdf', width=8, height=6)
ggplot(alpha_diversity, aes(x=Timepoint, y=Shannon, fill=Timepoint)) +
  geom_boxplot() +
  geom_jitter(width=0.2) +
  labs(title='Viral Shannon Diversity Across Timepoints',
       x='Timepoint', y='Shannon Index') +
  theme_minimal()
dev.off()

# Statistical test (ANOVA)
anova_result <- aov(Richness ~ Timepoint, data=alpha_diversity)
summary(anova_result)

# Post-hoc test (Tukey HSD)
tukey_result <- TukeyHSD(anova_result)
print(tukey_result)

7.2 Beta Diversity

# Calculate beta diversity (Bray-Curtis dissimilarity)
library(vegan)
library(ggplot2)

# Load abundance table
abundance <- read.delim('06_abundance/viral_coverage_all_samples.tsv', row.names=1)
abundance_t <- t(abundance)

# Calculate Bray-Curtis dissimilarity
bray_dist <- vegdist(abundance_t, method='bray')

# Perform NMDS ordination
set.seed(123)
nmds <- metaMDS(abundance_t, distance='bray', k=2, trymax=100)

# Extract NMDS scores
nmds_scores <- as.data.frame(scores(nmds))
nmds_scores$Sample <- rownames(nmds_scores)

# Merge with metadata
metadata <- read.delim('sample_metadata.tsv')
nmds_scores <- merge(nmds_scores, metadata, by='Sample')

# Plot NMDS
pdf('07_statistics/beta_diversity_nmds.pdf', width=10, height=8)
ggplot(nmds_scores, aes(x=NMDS1, y=NMDS2, color=Timepoint, shape=Replicate)) +
  geom_point(size=4) +
  stat_ellipse(aes(group=Timepoint), linetype=2) +
  labs(title=paste0('NMDS of Viral Communities (Stress=', round(nmds$stress, 3), ')'),
       x='NMDS1', y='NMDS2') +
  theme_minimal()
dev.off()

# PERMANOVA (test for significant differences between groups)
metadata_for_test <- metadata[match(rownames(abundance_t), metadata$Sample), ]
permanova <- adonis2(abundance_t ~ Timepoint, data=metadata_for_test, method='bray')
print(permanova)

# Save results
write.table(nmds_scores, '07_statistics/nmds_scores.tsv',
            sep='\t', row.names=FALSE, quote=FALSE)

7.3 Differential Abundance with DESeq2

# Identify vOTUs that change significantly across timepoints
library(DESeq2)
library(ggplot2)

# Load abundance table (raw counts)
counts <- read.delim('06_abundance/viral_coverage_all_samples.tsv', row.names=1)

# Round to integers (DESeq2 requires count data)
counts_int <- round(counts)

# Load metadata
metadata <- read.delim('sample_metadata.tsv', row.names=1)

# Ensure sample order matches
metadata <- metadata[colnames(counts_int), ]

# Create DESeq2 object
dds <- DESeqDataSetFromMatrix(
  countData = counts_int,
  colData = metadata,
  design = ~ Timepoint
)

# Run DESeq2
dds <- DESeq(dds)

# Extract results (T1 vs T3, for example)
res <- results(dds, contrast=c('Timepoint', 'T3', 'T1'))

# Filter significant vOTUs (padj < 0.05, |log2FC| > 1)
sig_votus <- subset(res, padj < 0.05 & abs(log2FoldChange) > 1)

# Save
write.csv(as.data.frame(sig_votus), '07_statistics/differential_abundance_T3_vs_T1.csv')

print(paste('Significantly different vOTUs:', nrow(sig_votus)))

# MA plot
pdf('07_statistics/deseq2_MA_plot.pdf', width=8, height=6)
plotMA(res, ylim=c(-5,5), main='Differential Abundance (T3 vs T1)')
dev.off()

# Volcano plot
pdf('07_statistics/deseq2_volcano_plot.pdf', width=8, height=6)
res_df <- as.data.frame(res)
res_df$significant <- res_df$padj < 0.05 & abs(res_df$log2FoldChange) > 1

ggplot(res_df, aes(x=log2FoldChange, y=-log10(padj), color=significant)) +
  geom_point(alpha=0.6) +
  scale_color_manual(values=c('grey', 'red')) +
  geom_hline(yintercept=-log10(0.05), linetype='dashed') +
  geom_vline(xintercept=c(-1, 1), linetype='dashed') +
  labs(title='Volcano Plot: Differential Abundance (T3 vs T1)',
       x='Log2 Fold Change', y='-Log10(Adjusted P-value)') +
  theme_minimal()
dev.off()

Step 8: Visualization and Heatmaps

Create Heatmap of Top vOTUs

library(pheatmap)

# Load abundance data
abundance <- read.delim('06_abundance/viral_abundance_relative.tsv', row.names=1)

# Select top 50 most abundant vOTUs (by mean abundance)
mean_abundance <- rowMeans(abundance)
top50_votus <- names(sort(mean_abundance, decreasing=TRUE)[1:50])

abundance_top50 <- abundance[top50_votus, ]

# Load metadata
metadata <- read.delim('sample_metadata.tsv', row.names=1)

# Create annotation for heatmap
annotation_col <- data.frame(
  Timepoint = metadata$Timepoint,
  Season = metadata$Season
)
rownames(annotation_col) <- rownames(metadata)

# Create heatmap
pdf('08_visualization/heatmap_top50_vOTUs.pdf', width=12, height=14)
pheatmap(
  log10(abundance_top50 + 1),  # Log transform
  annotation_col = annotation_col,
  cluster_rows = TRUE,
  cluster_cols = TRUE,
  show_rownames = FALSE,
  main = 'Top 50 vOTUs Across Samples (log10 relative abundance)',
  color = colorRampPalette(c('white', 'yellow', 'orange', 'red'))(50)
)
dev.off()

Step 9: Summary and Interpretation

Generate Comprehensive Report

# Create final summary directory
mkdir -p 09_summary

# Summary statistics
python3 << 'EOF'
import pandas as pd
import numpy as np

# Load all results
alpha_div = pd.read_csv('07_statistics/alpha_diversity.tsv', sep='\t')
deseq_res = pd.read_csv('07_statistics/differential_abundance_T3_vs_T1.csv', index_col=0)

print("=== Comparative Virome Analysis Summary ===\n")

print(f"Samples analyzed: {len(alpha_div)}")
print(f"Timepoints: {alpha_div['Timepoint'].nunique()}")
print(f"Replicates per timepoint: {alpha_div.groupby('Timepoint').size()[0]}")

print("\nAlpha diversity summary:")
print(f"  Mean richness: {alpha_div['Richness'].mean():.1f} ± {alpha_div['Richness'].std():.1f} vOTUs")
print(f"  Range: {alpha_div['Richness'].min()} - {alpha_div['Richness'].max()} vOTUs")

print("\nDifferential abundance (T3 vs T1):")
print(f"  Significantly different vOTUs: {len(deseq_res[deseq_res['padj'] < 0.05])}")
print(f"  Enriched in T3: {len(deseq_res[(deseq_res['padj'] < 0.05) & (deseq_res['log2FoldChange'] > 1)])}")
print(f"  Enriched in T1: {len(deseq_res[(deseq_res['padj'] < 0.05) & (deseq_res['log2FoldChange'] < -1)])}")
EOF

Expected Final Results

Typical Findings

  • Total viral contigs: 800-2,000
  • vOTUs (95% ANI): 300-800
  • Mean richness per sample: 100-300 vOTUs
  • Shannon diversity: 3.5-4.5
  • Significant temporal changes: 20-40% of vOTUs
  • Beta diversity: Clear separation by time (stress <0.15)

Biological Interpretation

Temporal patterns: - Viral diversity may peak in spring/summer - Community composition shifts with temperature - Some vOTUs are persistent, others are transient

Statistical significance: - PERMANOVA p < 0.05 indicates communities differ by time - DESeq2 identifies specific vOTUs driving differences - Replicate consistency validates findings

Troubleshooting

Problem: High Stress in NMDS

Solutions:

# Increase dimensions
nmds <- metaMDS(abundance_t, k=3, trymax=200)

# Try different distance metric
jaccard_dist <- vegdist(abundance_t, method='jaccard')

Problem: No Significant Differences

Possible causes: - True biological similarity - Insufficient replication - High variability

Solutions: - Increase sample size - Use non-parametric tests (Kruskal-Wallis) - Focus on effect sizes, not just p-values

Further Reading

  • Gregory, A. C., et al. (2019). "Marine DNA viral macro- and microdiversity from pole to pole." Cell, 177(5), 1109-1123.
  • Roux, S., et al. (2019). "Minimum information about an uncultivated virus genome (MIUViG)." Nature Biotechnology, 37(1), 29-37.