RNA-Seq Data Analysis

Lesson 8 of 13 · 10 min

Building the Count Matrix

A differential expression tool needs two aligned objects: a count matrix with one row per gene and one column per sample, and a metadata table describing what each sample is. In this lesson you import Salmon's per-sample quantifications, collapse transcript estimates up to genes, and confirm that every count column matches its metadata row. That aligned pairing is exactly what a downstream tool expects: edgeR builds a DGEList from the count matrix and its sample labels, and DESeq2's DESeqDataSetFromTximport() combines the whole tximport result object with this same, identically ordered metadata.

Locate the Salmon outputs

r
# Each sample has its own Salmon output folder containing quant.sf
samples <- c("ctrl_rep1", "ctrl_rep2", "ctrl_rep3",
             "treat_rep1", "treat_rep2", "treat_rep3")

files <- file.path("salmon", samples, "quant.sf")
names(files) <- samples

all(file.exists(files))
[1] TRUE

tximport reads each quant.sf and, given a transcript-to-gene table, sums the transcript-level estimates into gene-level counts. The tx2gene map must use the exact transcript IDs from the FASTA you indexed with Salmon. If your quant.sf keeps version suffixes such as .2, either match them in the map or pass ignoreTxVersion = TRUE.

r
library(tximport)

# Two-column map: transcript ID -> gene ID (IDs Salmon was indexed with)
tx2gene <- read.csv("tx2gene.csv", header = FALSE,
                    col.names = c("TXNAME", "GENEID"))

# Aggregate transcript-level quants up to gene level
txi <- tximport(files, type = "salmon", tx2gene = tx2gene)

counts <- txi$counts          # genes (rows) x samples (columns)
dim(counts)
head(round(counts), 3)
reading in files with read_tsv
1 2 3 4 5 6 
summarizing abundance
summarizing counts
summarizing length
[1] 21634     6
                ctrl_rep1 ctrl_rep2 ctrl_rep3 treat_rep1 treat_rep2 treat_rep3
ENSG00000000003       738       802       691        915        870        889
ENSG00000000419       521       498       540        467        503        488
ENSG00000000457       213       198       205        231        224        219

Pair with sample metadata

r
coldata <- data.frame(
  condition = factor(c("control", "control", "control",
                       "treated", "treated", "treated")),
  batch     = factor(c("A", "B", "A", "B", "A", "B")),
  row.names = c("ctrl_rep1", "ctrl_rep2", "ctrl_rep3",
                "treat_rep1", "treat_rep2", "treat_rep3")
)

# Force metadata rows into the same order as the matrix columns
coldata <- coldata[colnames(counts), ]

coldata
all(colnames(counts) == rownames(coldata))
           condition batch
ctrl_rep1    control     A
ctrl_rep2    control     B
ctrl_rep3    control     A
treat_rep1   treated     B
treat_rep2   treated     A
treat_rep3   treated     B
[1] TRUE

Never trust file order: a matrix whose columns are silently misaligned with coldata rows will fit a real model and hand you confidently wrong results. Always reorder with coldata[colnames(counts), ] and gate the next step on stopifnot(identical(colnames(counts), rownames(coldata))).

Try it yourself: Take four Salmon outputs (two wild-type, two knockout, split across two sequencing batches). Build the files vector and set names() to the sample IDs, run tximport with your tx2gene map to get a genes-by-samples matrix, then create a coldata data.frame whose row.names are those same sample IDs and whose columns are genotype and batch. Reorder it with coldata[colnames(counts), ] and confirm that identical(colnames(counts), rownames(coldata)) returns TRUE.