Counting reads in a metagenome assembly
This tutorial shows how to go from raw reads and a metagenome assembly to per-contig tables with bamcountrefs. You can produce either a simple count table or a set of metrics that are useful in metagenomics, such as abundance, coverage depth and breadth.
What you need
- A metagenome assembly in FASTA format, e.g.
assembly.fasta. It can be the contigs from MEGAHIT or metaSPAdes, a set of MAGs merged into one file, or a reference catalogue. - One or more samples of reads (Illumina paired-end in this example).
- minimap2, samtools and BamToCov. All three are available from BioConda:
conda create -n metacov -c conda-forge -c bioconda minimap2 samtools bamtocov
conda activate metacov
Here we assume a layout like this:
assembly.fasta
reads/
sample1_R1.fastq.gz sample1_R2.fastq.gz
sample2_R1.fastq.gz sample2_R2.fastq.gz
...
1. Index the assembly
minimap2 can map against the FASTA directly, but building the index once saves time when you map many samples:
minimap2 -x sr -d assembly.mmi assembly.fasta
The preset (-x sr) is stored in the index, so it must match the preset used
when mapping.
2. Map the reads and make sorted BAM files
Map each sample, then sort and index the output. bamcountrefs needs
coordinate-sorted, indexed BAM files, and all the samples must be mapped
against the same assembly.
mkdir -p bam
for R1 in reads/*_R1.fastq.gz; do
R2=${R1/_R1/_R2}
SAMPLE=$(basename "$R1" _R1.fastq.gz)
minimap2 -t 8 -a -x sr assembly.mmi "$R1" "$R2" \
| samtools sort -@ 4 -o bam/${SAMPLE}.bam -
samtools index bam/${SAMPLE}.bam
done
Which preset to use depends on the reads:
| Reads | minimap2 preset |
|---|---|
| Illumina short reads | -x sr |
| Oxford Nanopore | -x map-ont |
| PacBio HiFi | -x map-hifi |
For long reads, map a single FASTQ file per sample instead of a pair.
Each sample’s column in the output is named after its BAM file, up to the
first dot. bam/sample1.bam becomes sample1, so give each BAM file a unique,
meaningful name.
3a. Just a count table
The simplest output is the number of reads mapped to each contig in each sample, printed to standard output:
bamcountrefs bam/*.bam > counts.tsv
Contig sample1 sample2 sample3
contig_1 10234 8812 15020
contig_2 0 12 3
contig_3 5521 4980 6107
...
This is a good input for differential abundance tools that expect raw counts, such as DESeq2 or edgeR.
You can change the first column header with --tag (e.g. --tag MAG if each
reference is a genome bin) and speed things up with threads:
bamcountrefs --tag Contig -T 4 bam/*.bam > counts.tsv
Which reads are counted
By default, unmapped, secondary, supplementary, QC-failed and duplicate
alignments are skipped (-F 3844), so each read is counted at most once. You
can filter further:
# keep only confidently placed reads, and only proper pairs
bamcountrefs -Q 10 -P bam/*.bam > counts.tsv
Metagenome assemblies often contain closely related strains or repeated
elements. A read that maps equally well to several contigs gets MAPQ 0 and is
assigned to one of them at random. A MAPQ filter (-Q) removes these reads,
which makes the counts more specific but lowers them for contigs that share
sequence.
3b. Metrics for metagenomics
Raw counts don’t account for contig length or sequencing depth, and they don’t
tell you whether a contig is truly present or just collecting reads in one
small region (e.g. a conserved gene or a mobile element). To get more metrics,
pass an output basename with -o. bamcountrefs then writes one table per
metric:
mkdir -p results
bamcountrefs -o results/meta -T 4 \
--length --tpm --mean --trimmed-mean --covered-ratio --discov \
bam/*.bam
This creates:
| File | Metric | Use in metagenomics |
|---|---|---|
results/meta_counts.tsv |
raw read counts | input for differential abundance |
results/meta_length.tsv |
contig length | filter out short contigs, normalise |
results/meta_tpm.tsv |
TPM | relative abundance, comparable across samples |
results/meta_mean.tsv |
mean depth | contig coverage, e.g. for binning |
results/meta_trimmed_mean.tsv |
trimmed mean depth | depth that is robust to spikes from repeats |
results/meta_covered_fraction.tsv |
breadth of coverage | presence/absence of a contig or genome |
results/meta_discov.tsv |
DisCov score | how evenly reads are spread along the contig |
Or ask for everything at once:
bamcountrefs -o results/meta -T 4 --all-metrics bam/*.bam
Reading the metrics together
- Abundance: TPM corrects for both contig length and sample depth, so it’s
the best choice for comparing relative abundance across samples. RPKM
(
--rpkm) is also available. - Presence: a high count with a low breadth (covered fraction) usually means reads are piling up on a shared region, not that the organism is present. A common rule of thumb is to treat a genome as present only above a breadth threshold (e.g. 10–50%, depending on depth).
- Depth: the trimmed mean removes the lowest and highest coverage
positions (5% each by default, see
--trim-min/--trim-max). Like thetrimmed_meanin CoverM, it’s a more stable estimate of a genome’s depth than the plain mean. - Evenness: a low DisCov score flags patchy coverage, which can point to misassemblies, chimeric bins, or a related strain that only shares part of the sequence.
Breadth, trimmed mean, variance and DisCov track coverage at every base. They
need memory proportional to the total length of the contigs that receive
reads. With large assemblies, run fewer samples in parallel (-W) if memory
is tight. Counts, TPM, RPKM, mean and length need no extra memory.
MultiQC report
Add --multiqc to also write results/meta.mqc.txt, which
MultiQC can include in its report:
bamcountrefs -o results/meta --tpm --covered-ratio --multiqc bam/*.bam
Summary
# index once
minimap2 -x sr -d assembly.mmi assembly.fasta
# map, sort and index each sample
minimap2 -t 8 -a -x sr assembly.mmi s1_R1.fq.gz s1_R2.fq.gz \
| samtools sort -o bam/s1.bam - && samtools index bam/s1.bam
# 1) count table
bamcountrefs bam/*.bam > counts.tsv
# 2) metagenomics metrics
bamcountrefs -o results/meta --length --tpm --trimmed-mean --covered-ratio --discov bam/*.bam