Metagenomics Fundamentals

Lesson 8 of 13 · 12 min

Building the Feature/Abundance Table

A feature table is the sample-by-feature count matrix that every downstream analysis reads from: rows are features (ASVs, OTUs, or taxa) and columns are samples, with each cell holding the reads assigned to that feature in that sample. It never travels alone; it is joined to a sample metadata sheet keyed by sample ID and to a taxonomy table keyed by feature ID. Those two joins are what turn a raw count grid into something you can ask biological questions of.

text
#feature-id   S1     S2     S3     S4
ASV_0001      1204   0      58     932
ASV_0002      0      342    410    0
ASV_0003      77     12     0      145
bash
# Sanity-check the table before you trust it: depth, sparsity, per-sample counts.
biom summarize-table \
  --input-fp feature-table.biom \
  --output-fp table-summary.txt

head -n 20 table-summary.txt
Num samples: 4
Num observations: 3
Total count: 3180
Table density (fraction of non-zero entries): 0.667

Counts/sample summary:
 Min: 354.0
 Max: 1281.0
 Median: 772.500
 Mean: 795.000
 Std. dev.: 392.788
 Sample Metadata Categories: None provided
 Observation Metadata Categories: None provided

Counts/sample detail:
S2: 354.0
S3: 468.0
S4: 1077.0
S1: 1281.0

Filter, then collapse

bash
# Drop noise: features with fewer than 10 reads total, and features seen
# in fewer than 2 samples (a prevalence filter). Do this before normalizing.
qiime feature-table filter-features \
  --i-table table.qza \
  --p-min-frequency 10 \
  --p-min-samples 2 \
  --o-filtered-table table-filt.qza

# Aggregate ASVs up to genus (level 6 in a 7-rank kingdom-to-species taxonomy).
qiime taxa collapse \
  --i-table table-filt.qza \
  --i-taxonomy taxonomy.qza \
  --p-level 6 \
  --o-collapsed-table genus-table.qza

Read counts are compositional: the library size (total reads per sample) is set by the sequencer, not the biology, so an absolute count is meaningless on its own and only ratios between features carry information. Because every sample's parts are tied to that arbitrary total, an apparent rise in one feature forces others down, inducing spurious negative correlations and making raw counts unsafe to compare across samples. Filtering and collapsing change which parts you count, but the data stays compositional, so the next decision is how to put samples on a common footing.

Choosing a normalization

Rarefaction subsamples every sample down to a common depth, equalizing library size, and remains the standard input for alpha- and beta-diversity metrics, but it discards reads and adds sampling noise, so avoid it for differential abundance. Total-sum scaling (TSS), or relative abundance, divides each count by its sample total to give proportions that are easy to read and plot, yet the result is still compositional and breaks correlation and many standard tests. The centered log-ratio (CLR) takes the log of each count over the sample's geometric mean, moving the data into a real coordinate space where Euclidean distance and ordinary statistics behave, which is why tools like ALDEx2 and ANCOM-BC operate in log-ratio space.

r
# QIIME exports tables as features x samples; transpose to samples x features
# so that each normalization is computed per sample (across features).
counts <- t(as.matrix(read.delim("genus-table.tsv", row.names = 1, check.names = FALSE)))

# TSS: relative abundance, each row sums to 1.
tss <- sweep(counts, 1, rowSums(counts), "/")

# CLR: log-ratio to the per-sample geometric mean.
# Add a pseudocount first because log(0) is undefined.
pc  <- counts + 0.5
gm  <- exp(rowMeans(log(pc)))       # geometric mean of each sample
clr <- log(sweep(pc, 1, gm, "/"))  # log(x_i / geometric_mean)
round(clr[, 1:3], 2)
   g__Bacteroides  g__Prevotella  g__Faecalibacterium
S1           2.14          -1.87                 0.42
S2          -0.63           3.01                 1.55
S3           1.02           0.88                -2.31
S4           1.77          -0.44                 0.10

Try it yourself: Starting from genus-table.qza, rarefy it to an even depth with qiime feature-table rarefy --p-sampling-depth 1000, and separately compute the CLR matrix in R. Build a genus-by-sample bar chart from the TSS proportions and an Aitchison PCA with prcomp() on the CLR matrix, then note how the CLR ordination separates samples that the proportion bars make look nearly identical.