HUB (Hands-on
Understanding of Bioinformatics) Workshop
Bulk RNA-seq Data
Analysis
by
Bioinformatics Hub
thebiohub/bulkrnaseq Docker container on
your own laptop.docker pull --platform linux/amd64 thebiohub/bulkrnaseq
docker run -it -v ~/Desktop/BulkRNAseq:/workshop/data thebiohub/bulkrnaseq bash
docker run -it -v ${HOME}\Desktop\BulkRNAseq:/workshop/data thebiohub/bulkrnaseq bash
pwd
ls
tree -L 2 data

STAR --version
samtools --version

ls data/fastq

wo and press ‘Tab’ to autocomplete the path.--genomeSAindexNbases sets the length of the seed
lookup table in STAR’s genome index. Set it to 12 for
the workshop to reduce runtime and memory usage; the default of 14 is
designed for whole-genome data.mkdir -p /workshop/data/results/star/index
STAR --runMode genomeGenerate \
--runThreadN 2 \
--genomeDir /workshop/data/results/star/index \
--genomeFastaFiles /workshop/data/reference/chrX.fa \
--sjdbGTFfile /workshop/data/reference/chrX.gtf \
--sjdbOverhang 75 \
--genomeSAindexNbases 12
ls /workshop/data/results/star/index

Fallback — copy out the prebuilt STAR index. If index generation fails on your machine, copy the prebuilt index we prepared in advance and continue:
# Note: This is a fallback step and is only needed if the STAR index generation fails.
cp -rf /workshop/star/index/. /workshop/data/results/star/index/
ls /workshop/data/results/star/index
--quantMode GeneCounts — tallies reads per gene
in the same pass.STAR --runThreadN 2 \
--genomeDir /workshop/data/results/star/index \
--readFilesIn /workshop/data/fastq/ERR188245_chrX_1.fastq.gz \
/workshop/data/fastq/ERR188245_chrX_2.fastq.gz \
--readFilesCommand zcat \
--outSAMtype BAM SortedByCoordinate \
--quantMode GeneCounts \
--outFileNamePrefix /workshop/data/results/star/ERR188245_

STAR --runThreadN 2 \
--genomeDir /workshop/data/results/star/index \
--readFilesIn /workshop/data/fastq/ERR188257_chrX_1.fastq.gz \
/workshop/data/fastq/ERR188257_chrX_2.fastq.gz \
--readFilesCommand zcat \
--outSAMtype BAM SortedByCoordinate \
--quantMode GeneCounts \
--outFileNamePrefix /workshop/data/results/star/ERR188257_

Log.final.out is STAR’s own summary of how the run
went.cat /workshop/data/results/star/ERR188245_Log.final.out

grep "Uniquely mapped reads %" /workshop/data/results/star/ERR188245_Log.final.out
grep "Uniquely mapped reads %" /workshop/data/results/star/ERR188257_Log.final.out

head /workshop/data/results/star/ERR188245_ReadsPerGene.out.tab

tail -n +5 /workshop/data/results/star/ERR188245_ReadsPerGene.out.tab | awk '$2 > 0' | wc -l
tail -n +5 /workshop/data/results/star/ERR188245_ReadsPerGene.out.tab | wc -l

.bai lets tview
and region queries jump straight to a position instead of scanning the
whole file.samtools index /workshop/data/results/star/ERR188245_Aligned.sortedByCoord.out.bam
ls /workshop/data/results/star

samtools view -H /workshop/data/results/star/ERR188245_Aligned.sortedByCoord.out.bam
samtools view /workshop/data/results/star/ERR188245_Aligned.sortedByCoord.out.bam | head -3
samtools flagstat /workshop/data/results/star/ERR188245_Aligned.sortedByCoord.out.bam

samtools tview /workshop/data/results/star/ERR188245_Aligned.sortedByCoord.out.bam /workshop/data/reference/chrX.fa
tview:g go to region
chrX:73820651
n color by nucleotide (A green, C cyan, G magenta, T red, N blue)
b color by base quality (>=30 white, 20-29 yellow, 10-19 green, 0-9 blue)
m color by mapping quality (>=30 white, 20-29 yellow, 10-19 green, 0-9 blue)
q quit / leave

gene_id and one column for each sample).printf "gene_id\tERR188245\tERR188257\n" > /workshop/data/results/star/gene_counts.tsv
cat /workshop/data/results/star/gene_counts.tsv

gene_id plus one count column per sample.paste <(tail -n +5 /workshop/data/results/star/ERR188245_ReadsPerGene.out.tab | cut -f1,2) \
<(tail -n +5 /workshop/data/results/star/ERR188257_ReadsPerGene.out.tab | cut -f2) \
>> /workshop/data/results/star/gene_counts.tsv
head /workshop/data/results/star/gene_counts.tsv
wc -l /workshop/data/results/star/gene_counts.tsv

salmon --version
gffread --version

chrX.fa) and the annotation
(chrX.gtf) — the same pair STAR used in Pipeline A.grep -v "^#" /workshop/data/reference/chrX.gtf | head -3
awk -F'\t' '$3 == "gene"' /workshop/data/reference/chrX.gtf | wc -l

gffread:gffread -w /workshop/data/reference/chrX.transcripts.fa \
-g /workshop/data/reference/chrX.fa \
/workshop/data/reference/chrX.gtf
grep -c ">" /workshop/data/reference/chrX.transcripts.fa
head /workshop/data/reference/chrX.transcripts.fa

salmon index \
-p 2 \
-t /workshop/data/reference/chrX.transcripts.fa \
-i /workshop/data/results/salmon/index
ls /workshop/data/results/salmon/index

Fallback — copy out the prebuilt Salmon index. If index generation fails, copy the prebuilt index we prepared in advance and continue:
# Note: This is a fallback step and is only needed if the Salmon index generation fails.
cp -rf /workshop/salmon/index/. /workshop/data/results/salmon/index/
-l option specifies the library type (how the reads
were prepared and sequenced).-l A tells Salmon to automatically
detect the library type from the input reads, so you don’t need
to specify it manually.salmon quant -i /workshop/data/results/salmon/index \
-p 2 \
-l A \
-1 /workshop/data/fastq/ERR188245_chrX_1.fastq.gz \
-2 /workshop/data/fastq/ERR188245_chrX_2.fastq.gz \
-o /workshop/data/results/salmon/ERR188245
head /workshop/data/results/salmon/ERR188245/quant.sf

quant.sf columns:Name transcript ID
Length transcript length in bp
EffectiveLength length adjusted for the fragment-length distribution;
this is what converts between counts and TPM
TPM Transcripts Per Million; relative abundance, sums to
1,000,000 across all transcripts
NumReads estimated reads assigned to the transcript (fractional,
because multimapping reads are split probabilistically)
salmon quant -i /workshop/data/results/salmon/index \
-p 2 \
-l A \
-1 /workshop/data/fastq/ERR188257_chrX_1.fastq.gz \
-2 /workshop/data/fastq/ERR188257_chrX_2.fastq.gz \
-o /workshop/data/results/salmon/ERR188257

grep -i "Mapping rate" /workshop/data/results/salmon/ERR188245/logs/salmon_quant.log
grep -i "Mapping rate" /workshop/data/results/salmon/ERR188257/logs/salmon_quant.log

quant.sf is per transcript; most analyses want per
gene. The bridge is a tx2gene map — three columns,
TXID GENEID GENESYMBOL — built from the same GTF. gffread’s
--table option prints exactly these columns; add the header
first:printf "TXID\tGENEID\tGENESYMBOL\n" > /workshop/data/reference/tx2gene.tsv
cat /workshop/data/reference/tx2gene.tsv

gene_name in the GTF, and gffread falls back to the ID.
Only TXID and GENEID drive the rollup, so this is cosmetic.gffread /workshop/data/reference/chrX.gtf \
--table @id,@geneid,gene_name \
>> /workshop/data/reference/tx2gene.tsv
head /workshop/data/reference/tx2gene.tsv
wc -l /workshop/data/reference/tx2gene.tsv

tximport reads each sample’s quant.sf (per
transcript) and sums it to per gene using the tx2gene map, producing one
gene × sample count matrix.STAR --quantMode GeneCounts (integer counts), the other via
salmon + tximport (tximport-corrected, fractional counts). Same shape,
two independent routes.more /workshop/scripts/tximport_rollup.R
Rscript /workshop/scripts/tximport_rollup.R
head /workshop/data/results/salmon/gene_counts.tsv

gene_id. Plot one against the other — if they
agree, points hug the y = x line. X-axis is STAR, Y-axis is Salmon; each
dot is one chrX gene. Counts span orders of magnitude, so transform with
asinh — like a log at large counts but defined at zero
(asinh(0) = 0), so no pseudocount is needed.R
star <- read.delim("/workshop/data/results/star/gene_counts.tsv")
salmon <- read.delim("/workshop/data/results/salmon/gene_counts.tsv")
# keep genes quantified by BOTH pipelines, matched on gene_id
m <- merge(star, salmon, by = "gene_id", suffixes = c(".star", ".salmon"))
cat(nrow(m), "chrX genes shared by both pipelines\n")
pdf("/workshop/data/results/star_vs_salmon.pdf", width = 10, height = 5)
par(mfrow = c(1, 2))
for (s in c("ERR188245", "ERR188257")) {
x <- asinh(m[[paste0(s, ".star")]])
y <- asinh(m[[paste0(s, ".salmon")]])
plot(x, y, pch = 16, cex = 0.6, col = rgb(0, 0, 0, 0.4),
xlab = paste("STAR asinh(count) -", s),
ylab = paste("Salmon asinh(count) -", s))
abline(0, 1, col = "red")
legend("topleft", bty = "n", legend = paste("r =", round(cor(x, y), 3)))
}
dev.off()
q("no")

--quantMode GeneCounts will not count a read
that overlaps two genes (it goes to N_ambiguous), so the
nested pseudogene gets 0. Salmon maps to transcripts
and apportions a share to the pseudogene isoform, so it reports
~82.RLIM ENSG00000131263 chrX:74,582,976-74,614,646 (31.7 kb)
PABPC1P3 ENSG00000230673 chrX:74,583,088-74,583,546 (458 bp, nested)

grep -E "ENSG00000131263|ENSG00000230673" /workshop/data/results/star/gene_counts.tsv
grep -E "ENSG00000131263|ENSG00000230673" /workshop/data/results/salmon/gene_counts.tsv

samtools view -c /workshop/data/results/star/ERR188245_Aligned.sortedByCoord.out.bam chrX:74583088-74583546
samtools view -c -q 255 /workshop/data/results/star/ERR188245_Aligned.sortedByCoord.out.bam chrX:74583088-74583546
