RNAseq QC

Matthew Taliaferro

RNA Bioscience Initiative | CU Anschutz

2026-09-25

Learning Objectives

By the end of the class, you should be able to:

  • Understand how salmon can be used to quantify gene expression given an RNAseq dataset

  • Collapse transcript-level quantifications to gene level quantifications using tximport

  • Deduce the relationships of samples to each other using hierarchical clustering and PCA

Rigor & Reproducibility

As with all computational experiments (yes, they are experiments, don’t let your pipette-toting friends tell you otherwise), keeping track of what you did is key. In the old days, I kept a written notebook of commands that I ran. Sounds silly, but there were many times that I went back to that notebook to see exactly what the parameters were for a given run using a piece of software.

Today, there are better options. You are using one of the better ones right now. Notebooks, including RMarkdown (mainly for R) and Jupyter (mainly for Python), are a great way to keep track of what you did as well as give justification or explanation for your analyses using plain ’ol English.

Trust me, one day you will be glad you used them. The Methods section of your paper is never fun to write without them.

Overview

In this class, we will examine RNAseq data collected over a time-course of differentiation from mouse embryonic stem cells to cortical glutamatergic neurons (Hubbard et al, F1000 Research (2013)). In this publication, the authors differentiated mESCs to neurons using a series of in vitro culture steps over a period of 37 days. During this timecourse, samples were extracted at selected intervals for transcriptome analysis. Importantly, for each timepoint, either 3 or 4 samples were taken for RNA extraction, library preparation and sequencing. This allows us to efficiently use the statistical frameworks provided by the DESeq2 package to identify genes whose RNA expression changes across the timecourse.

Experiment design

Cells were grown in generic differentiation-promoting media (LIF-) for 8 days until aggreates were dissociated and replated in neuronal differentiation media. This day of replating was designated as in vitro day 0 (DIV0). The timepoints taken before this replating therefore happened at “negative” times (DIV-8 and DIV-4). Because naming files with dashes or minus signs can cause problems, these samples are referred to as DIVminus8 and DIVminus4. Following the replating, samples were taken at days 1, 7, 16, 21, and 28 (DIV1, DIV7, DIV16, DIV21, and DIV28).

QC overview

Today we will focus on some quality control steps that are good ideas to do for every RNAseq dataset you encounter, whether produced by yourself or someone else.

Goal

Today we will focus on some Quality Control steps that are good ideas to do for every RNA-seq data set you encounter, whether produced by yourself or someone else.

Quantifying transcript expression with salmon

We recently learned about the RNAseq quantification tool salmon. We won’t rehash the details here about how salmon works. For our purposes, we just need to know that salmon reads in a fastq file of sequencing reads and a fasta file of transcript sequences to be quantified. Let’s take a look at this fasta file of transcripts:

head -n 20 data/block-rna/gencodecomprehensive.vM17.allcdna_subset.fa

Looks like we have ensembl transcript IDs, which is a good idea. I can tell because they start with ‘ENS’. Using ensembl IDs as transcript names will allow us to later collate transcript expression levels into gene expression levels using a database that relates transcripts and genes. More on that later.

Making a transcriptome index

The first step in quantifying these transcripts is to make an index from them. This is done as follows:

salmon index -t <transcripts.fa> -i <transcripts.idx> --type quasi -k <k>

Here, transcripts.fa is a path to our fasta file, transcripts.idx is the name of the index that will be created, and k is the length of the kmers that will be used in the hash table related kmers and transcripts. k is the length of the minimum accepted match for a kmer in a read and a kmer in a transcript. Longer kmers (higher values of k) will therefore be more stringent, and lowering k may improve mapping sensitivity at the cost of some specificity. You may also see here how read lengths can influence what value for k you should choose.

Consider an experiment where we had 25 nt reads (this was true wayyyyyy back in the old, dark days of high-throughput sequencing). What’s going to happen if I quantify these reads using an index where the kmer size was set to 29? Well, nothing will align. The index has represented the transcriptome in 29 nt chunks. However, no read will match to these 29mers because there are no 29mers in these reads! As a general rule of thumb, for reads 75 nt and longer (which is the bulk of the data produced nowadays), a good value for k that maximizes both specificity and sensitivity is 31. However, datasets that you may retrieve from the internet, particularly older ones, may have shorter read lengths, so keep this is mind when defining k.

Quantifying reads against this index

Once we have our index, we can quantify transcripts in the index using reads from our fastq files.

salmon quant --libType A -p 8 --seqBias --gcBias --validateMappings -1 <forwardreads.fastq> -2 <reversereads.fastq> -o <outputname> --index <transcripts.idx>

In this command, our forward and reverse read fastq files are supplied to -1 and -2, respectively. If the experiment produced single end reads, -2 is omitted. <transcripts.idx> is the path to the index produced in the previous step. I’m not going to go through the rest of the flags used here, but their meanings as well as other options can be found here.

Salmon outputs

Let’s take a look at what salmon spits out. The first file we will look at is a log that is found at /logs/salmon_quant.log. This file contains info about the quantification, but there’s one line of this file in particular that we are interested in. It lets us know how many of the reads in the fastq file that salmon found a home for in the transcriptome fasta.

less data/block-rna/differentiation_salmonouts/DIV0.Rep1/logs/salmon_quant.log

There are a lot of lines in this file, but really only one that we are interested in. We want the one that tells us the “Mapping rate.” How could we easily and efficiently look at the mapping rates of all our samples? Grep!

#Get the mapping rates for all samples
#In each log file, the line that we are interested in contains the string 'Mapping ' (notice the space)
grep 'Mapping ' data/block-rna/differentiation_salmonouts/*/logs/salmon_quant.log

Moving from transcript quantifications to gene quantifications

As we discussed, salmon quantifies transcripts, not genes. However, genes are made up of transcripts, so we can calculate gene expression values from transcript expression values if we knew which transcripts belonged to which genes.

Relating genes and transcripts

We can get this relationships between transcripts and genes through AnnotationHub.

AnnotationHub has many tables that relate genes, transcripts, and other useful data including gene biotypes and gene ontology categories, even across species. Let’s use it here to get a table of genes and transcripts for the mouse genome.

ah <- AnnotationHub(ask = FALSE)
# Helper: pull AnnotationHub metadata into a tibble and grab the
# most recent EnsDb hit for a given species
get_latest_ensdb <- function(ah, species) {
  query(ah, c("EnsDb", species)) |>
    mcols() |>
    as_tibble(rownames = "ah_id") |>
    arrange(desc(rdatadateadded)) |>
    dplyr::slice(1)
}

mouse_hit <- get_latest_ensdb(ah, "Mus musculus") |>
  select(ah_id, title, genome, rdatadateadded)

edb_mouse <- ah[[mouse_hit$ah_id]]
downloading 1 resources
retrieving 1 resource
loading from cache

I encourage you to see what is in this database, but for now we are only going to look for transcript IDs and gene IDs.

head(mcols(ah))
DataFrame with 6 rows and 15 columns
                 title dataprovider      species taxonomyid
           <character>  <character>  <character>  <integer>
AH5012 Chromosome Band         UCSC Homo sapiens       9606
AH5013     STS Markers         UCSC Homo sapiens       9606
AH5014     FISH Clones         UCSC Homo sapiens       9606
AH5015     Recomb Rate         UCSC Homo sapiens       9606
AH5016    ENCODE Pilot         UCSC Homo sapiens       9606
AH5017     Map Contigs         UCSC Homo sapiens       9606
            genome            description
       <character>            <character>
AH5012        hg19 GRanges object from ..
AH5013        hg19 GRanges object from ..
AH5014        hg19 GRanges object from ..
AH5015        hg19 GRanges object from ..
AH5016        hg19 GRanges object from ..
AH5017        hg19 GRanges object from ..
       coordinate_1_based             maintainer
                <integer>            <character>
AH5012                  1 Marc Carlson <mcarls..
AH5013                  1 Marc Carlson <mcarls..
AH5014                  1 Marc Carlson <mcarls..
AH5015                  1 Marc Carlson <mcarls..
AH5016                  1 Marc Carlson <mcarls..
AH5017                  1 Marc Carlson <mcarls..
       rdatadateadded          preparerclass
          <character>            <character>
AH5012     2013-03-26 UCSCFullTrackImportP..
AH5013     2013-03-26 UCSCFullTrackImportP..
AH5014     2013-03-26 UCSCFullTrackImportP..
AH5015     2013-03-26 UCSCFullTrackImportP..
AH5016     2013-03-26 UCSCFullTrackImportP..
AH5017     2013-03-26 UCSCFullTrackImportP..
                               tags  rdataclass
                             <AsIs> <character>
AH5012      cytoBand,UCSC,track,...     GRanges
AH5013        stsMap,UCSC,track,...     GRanges
AH5014    fishClones,UCSC,track,...     GRanges
AH5015    recombRate,UCSC,track,...     GRanges
AH5016 encodeRegions,UCSC,track,...     GRanges
AH5017        ctgPos,UCSC,track,...     GRanges
                    rdatapath              sourceurl
                  <character>            <character>
AH5012 goldenpath/hg19/data.. rtracklayer://hgdown..
AH5013 goldenpath/hg19/data.. rtracklayer://hgdown..
AH5014 goldenpath/hg19/data.. rtracklayer://hgdown..
AH5015 goldenpath/hg19/data.. rtracklayer://hgdown..
AH5016 goldenpath/hg19/data.. rtracklayer://hgdown..
AH5017 goldenpath/hg19/data.. rtracklayer://hgdown..
        sourcetype
       <character>
AH5012  UCSC track
AH5013  UCSC track
AH5014  UCSC track
AH5015  UCSC track
AH5016  UCSC track
AH5017  UCSC track

Using AnnotationHub

A lot of stuff for a lot of species! Perhaps we want to limit it to see which ones are relevant to mouse.

mcols(ah) |>
  as.data.frame() |>
  filter(species == 'Mus musculus') |>
  head()
              title dataprovider      species taxonomyid
AH6057      Contigs         UCSC Mus musculus      10090
AH6058     Assembly         UCSC Mus musculus      10090
AH6059          Gap         UCSC Mus musculus      10090
AH6060 GRC Incident         UCSC Mus musculus      10090
AH6061   UCSC Genes         UCSC Mus musculus      10090
AH6062   Alt Events         UCSC Mus musculus      10090
       genome                                   description
AH6057   mm10      GRanges object from UCSC track 'Contigs'
AH6058   mm10     GRanges object from UCSC track 'Assembly'
AH6059   mm10          GRanges object from UCSC track 'Gap'
AH6060   mm10 GRanges object from UCSC track 'GRC Incident'
AH6061   mm10   GRanges object from UCSC track 'UCSC Genes'
AH6062   mm10   GRanges object from UCSC track 'Alt Events'
       coordinate_1_based                        maintainer
AH6057                  1 Marc Carlson <mcarlson@fhcrc.org>
AH6058                  1 Marc Carlson <mcarlson@fhcrc.org>
AH6059                  1 Marc Carlson <mcarlson@fhcrc.org>
AH6060                  1 Marc Carlson <mcarlson@fhcrc.org>
AH6061                  1 Marc Carlson <mcarlson@fhcrc.org>
AH6062                  1 Marc Carlson <mcarlson@fhcrc.org>
       rdatadateadded               preparerclass
AH6057     2013-03-26 UCSCFullTrackImportPreparer
AH6058     2013-03-26 UCSCFullTrackImportPreparer
AH6059     2013-03-26 UCSCFullTrackImportPreparer
AH6060     2013-03-26 UCSCFullTrackImportPreparer
AH6061     2013-03-26 UCSCFullTrackImportPreparer
AH6062     2013-03-26 UCSCFullTrackImportPreparer
               tags rdataclass
AH6057 assembly....    GRanges
AH6058 gold, UC....    GRanges
AH6059 gap, UCS....    GRanges
AH6060 grcIncid....    GRanges
AH6061 knownGen....    GRanges
AH6062 knownAlt....    GRanges
                                                rdatapath
AH6057 goldenpath/mm10/database/assemblyFrags_0.0.1.RData
AH6058          goldenpath/mm10/database/gold_0.0.1.RData
AH6059           goldenpath/mm10/database/gap_0.0.1.RData
AH6060 goldenpath/mm10/database/grcIncidentDb_0.0.1.RData
AH6061     goldenpath/mm10/database/knownGene_0.0.1.RData
AH6062      goldenpath/mm10/database/knownAlt_0.0.1.RData
                                                                          sourceurl
AH6057 rtracklayer://hgdownload.cse.ucsc.edu/goldenpath/mm10/database/assemblyFrags
AH6058          rtracklayer://hgdownload.cse.ucsc.edu/goldenpath/mm10/database/gold
AH6059           rtracklayer://hgdownload.cse.ucsc.edu/goldenpath/mm10/database/gap
AH6060 rtracklayer://hgdownload.cse.ucsc.edu/goldenpath/mm10/database/grcIncidentDb
AH6061     rtracklayer://hgdownload.cse.ucsc.edu/goldenpath/mm10/database/knownGene
AH6062      rtracklayer://hgdownload.cse.ucsc.edu/goldenpath/mm10/database/knownAlt
       sourcetype
AH6057 UCSC track
AH6058 UCSC track
AH6059 UCSC track
AH6060 UCSC track
AH6061 UCSC track
AH6062 UCSC track

Using AnnotationHub

OK lets get data about mouse transcripts. I’m going to also add the gene_name attribute.

t2g <- transcripts(edb_mouse,
                              columns = c("tx_id", "gene_id", "gene_name"),
                              return.type = "DataFrame") |>
  as_tibble() |>
  dplyr::rename(ensembl_transcript_id = tx_id, ensembl_gene_id = gene_id)

head(t2g)
# A tibble: 6 × 3
  ensembl_transcript_id ensembl_gene_id    gene_name
  <chr>                 <chr>              <chr>    
1 ENSMUST00000000001    ENSMUSG00000000001 Gnai3    
2 ENSMUST00000000003    ENSMUSG00000000003 Pbsn     
3 ENSMUST00000000010    ENSMUSG00000020875 Hoxb9    
4 ENSMUST00000000028    ENSMUSG00000000028 Cdc45    
5 ENSMUST00000000033    ENSMUSG00000048583 Igf2     
6 ENSMUST00000000049    ENSMUSG00000000049 Apoh     

Using AnnotationHub

Alright this looks good! Although the gene_name field could be useful, for our purposes today, we don’t really need it, and the tools we are using don’t expect it.

t2g <- t2g |>
  select(ensembl_transcript_id, ensembl_gene_id)

Getting gene level expression data with tximport

Now that we have our table relating transcripts and genes, we can give it to tximport to have it calculate gene-level expression data from our transcript-level expression data.

First, we have to tell it where the salmon quantification files (the quant.sf.gz files) are. Here’s what our directory structure that contains these files looks like:

Gene expression data with tximport

# The directory where all of the sample-specific salmon subdirectories live

metadata <- tibble(
  salmon_dirs = fs::dir_ls(
    here("data/block-rna/differentiation_salmonouts"),
    recurse = TRUE,
    glob = "*quant.sf"
  )
) |>
  mutate(
    sample_id = fs::path_file(fs::path_dir(salmon_dirs))
  ) |>
  filter(str_starts(sample_id, "DIV")) |>
  separate_wider_delim(
    col = sample_id,
    delim = ".",
    names = c("samp", "rep"),
    too_few = "align_start",
    cols_remove = FALSE
  ) |>
  mutate(rep = str_remove(rep, "Rep")) |>
  column_to_rownames("sample_id")

# Add the sample_id column back from rownames
metadata$sample_id <- rownames(metadata)

Gene expression data with tximport

You can see that we got a list of sample names and the absolute path to each sample’s quantification file.

Now we are ready to run tximport

tximport is going to want paths to all the quantification files (salm_dirs) and a table that relates transcripts to genes (t2g). Luckily, we happen to have those exact two things.

salmdir <- metadata$salmon_dirs
names(salmdir) <- metadata$sample_id


txi <- tximport(
  files = salmdir,
  type = "salmon",
  tx2gene = t2g,
  dropInfReps = TRUE,
  countsFromAbundance = "lengthScaledTPM"
)
reading in files with read_tsv
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
transcripts missing from tx2gene: 4374
summarizing abundance
summarizing counts
summarizing length

Gene expression data with tximport

Notice how we chose lengthscaledTPM for our abundance measurement. This is going to give us TPM values (transcripts per million) for expression in the $abundance slot. Let’s check out what we have now.

tpms <- as.data.frame(txi$abundance) |>
  set_names(rownames(metadata)) |>
  rownames_to_column(var = "ensembl_gene_id")

gt(tpms[1:50, ])
ensembl_gene_id DIV0.Rep1 DIV0.Rep2 DIV0.Rep3 DIV1.Rep1 DIV1.Rep2 DIV1.Rep3 DIV1.Rep4 DIV16.Rep1 DIV16.Rep2 DIV16.Rep3 DIV16.Rep4 DIV21.Rep1 DIV21.Rep2 DIV21.Rep3 DIV21.Rep4 DIV28.Rep1 DIV28.Rep2 DIV28.Rep3 DIV28.Rep4 DIV7.Rep1 DIV7.Rep2 DIV7.Rep3 DIV7.Rep4 DIVminus4.Rep1 DIVminus4.Rep2 DIVminus4.Rep3 DIVminus8.Rep1 DIVminus8.Rep2 DIVminus8.Rep3 DIVminus8.Rep4
ENSMUSG00000000001 148.004511 145.539529 164.680183 142.713005 138.042672 137.027499 123.894516 41.689798 41.554126 41.002841 39.522314 28.162815 29.694158 29.333682 27.956845 17.960041 17.979483 18.102228 19.590551 76.088940 76.669016 79.505738 76.038479 106.634101 106.395094 109.997598 92.799612 93.563524 95.500337 101.951963
ENSMUSG00000000003 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
ENSMUSG00000000028 34.180035 36.215924 39.486489 16.894844 15.391383 18.171282 16.654062 1.067147 1.270428 1.109391 1.136970 1.939558 1.003108 1.245186 1.086912 1.303824 0.935135 0.864451 0.862209 6.582942 6.752942 5.547107 5.510259 45.614077 47.851930 47.516125 59.718765 57.981259 57.935839 41.663343
ENSMUSG00000000031 7.213808 6.431925 13.253322 3.301713 2.539161 3.591509 2.798164 93.312776 56.314466 62.958855 68.565195 32.037271 37.465959 40.581328 24.780757 67.818115 64.333586 30.408290 60.198406 151.244212 137.227925 169.615059 150.599359 0.914481 1.402704 1.317939 0.177770 0.215726 0.320948 36.425779
ENSMUSG00000000037 5.877237 5.001733 7.579149 2.861842 3.176129 2.457556 3.591170 0.319281 0.708167 0.578467 0.513434 0.186198 0.260270 0.258075 0.585622 0.615895 0.133394 0.197050 1.005331 2.750903 1.827190 2.446732 2.894751 2.196733 2.309187 2.213591 3.187886 2.507155 2.624140 2.281871
ENSMUSG00000000049 0.000000 0.046208 0.000000 0.000000 0.000000 0.000000 0.411807 0.213589 0.118890 1.024154 0.628760 0.303218 0.449928 0.262019 0.105047 0.567535 0.649720 0.515246 0.360877 0.188254 0.050654 0.277837 0.189814 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
ENSMUSG00000000056 31.553583 28.610456 19.741875 59.046908 51.324548 54.015893 43.897451 36.575044 47.171198 41.459018 38.403920 55.170239 37.441141 44.043899 45.852392 38.906362 40.362321 44.933634 42.356811 41.828967 46.812515 44.172289 46.834670 14.509458 18.656028 16.335093 10.671419 10.666954 12.386288 8.077939
ENSMUSG00000000058 0.420304 0.507728 0.602757 0.357594 0.504091 0.498006 0.565744 0.931596 0.632998 0.606530 0.503290 0.292719 0.224968 0.401341 0.428359 0.490118 0.200389 0.038911 0.399079 0.194399 0.102993 0.063154 0.130644 0.163821 0.162316 0.251082 0.693347 0.201366 0.527565 0.482299
ENSMUSG00000000078 52.604855 64.802703 76.892914 60.446343 60.861211 61.059568 50.159693 32.474649 28.287563 27.391212 29.511742 19.632508 19.942257 20.182975 19.363735 16.845197 16.128839 18.828771 15.732413 52.939918 52.709453 54.334181 50.804497 51.427093 49.900737 53.274561 19.669879 19.472875 20.216492 21.591729
ENSMUSG00000000085 60.617248 59.182914 61.623794 72.657499 71.506733 75.816856 63.507273 36.498680 31.450681 36.983697 34.558348 31.893252 35.692047 31.095232 34.751542 36.339896 32.474173 38.456832 39.182603 52.494911 43.715591 45.720249 46.351620 9.949776 11.703136 9.320286 13.843137 11.699411 13.486194 10.366294
ENSMUSG00000000088 372.546920 364.764405 381.963748 389.617591 388.204349 413.080094 533.720843 882.254461 918.864762 881.567068 952.656933 1064.561469 1018.564149 1023.266589 935.493316 944.628320 1102.492902 1011.428801 993.369153 703.366663 749.229000 711.055019 744.360315 602.694088 539.095644 568.529023 715.919453 723.040511 713.534783 795.564123
ENSMUSG00000000093 18.748818 26.367729 28.740489 18.171262 17.023064 19.681817 21.696499 1.748123 1.159551 1.244226 1.286737 1.859175 1.536283 1.490581 1.446475 3.138448 3.362518 3.197714 3.445495 1.960168 1.812696 2.626110 2.401155 1.641559 1.120127 1.520160 0.123672 0.064585 0.118030 0.062646
ENSMUSG00000000094 0.114764 0.197628 0.237980 0.067150 0.050183 0.025762 0.000000 0.000000 0.000000 0.020946 0.000000 0.023273 0.000000 0.020613 0.020391 0.000000 0.000000 0.000000 0.000000 0.253309 0.359092 0.191826 0.280499 0.644665 0.508711 0.638906 1.610289 2.043695 1.513517 1.061169
ENSMUSG00000000103 0.000000 0.000000 0.019350 0.019932 0.000000 0.000000 0.000000 0.024280 0.000000 0.000000 0.027595 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.022570 0.020990 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.049946
ENSMUSG00000000120 18.187330 17.621063 16.764680 65.095814 63.742121 70.050947 65.437767 7.138900 6.728019 6.471314 7.243701 3.043655 3.779647 4.010854 3.292345 1.055312 1.336741 1.401802 1.323000 7.879629 8.472306 7.826161 8.344422 1.302789 1.494076 1.100786 2.720008 2.666598 3.080750 7.171139
ENSMUSG00000000125 9.426741 9.885487 5.404755 2.614957 1.888344 2.419112 2.543413 1.836920 2.186343 2.081065 2.033197 1.815480 1.714785 1.591194 1.707502 1.388731 1.150249 1.311930 1.489738 2.613648 2.668347 2.770951 2.336912 1.482033 1.217406 1.473157 0.168666 0.267824 0.162984 0.439311
ENSMUSG00000000126 1.215801 0.746274 0.814387 0.893695 0.726967 0.795409 0.811422 1.698931 0.816782 1.232844 1.175287 0.776041 0.671299 0.527801 0.856885 0.881084 0.587611 0.480369 0.599140 1.920124 1.992736 2.643161 2.442442 1.243716 0.787857 1.154595 1.480120 1.685512 1.662030 1.757216
ENSMUSG00000000127 13.113348 14.260035 14.074681 16.162555 17.427828 17.565289 11.863558 12.271221 14.241964 13.551247 14.347367 12.813797 12.260097 11.685471 12.283357 8.849005 8.757533 8.775956 9.371477 16.306853 15.553249 15.896542 15.505971 11.131771 11.417468 12.892073 12.916885 12.422329 12.138589 11.783150
ENSMUSG00000000131 55.485697 54.448611 62.549063 71.704310 71.301569 71.997625 85.422041 53.653944 54.569370 54.041498 53.203853 56.560171 55.599344 57.503726 56.142399 56.062512 54.730314 56.114317 55.657729 64.772660 60.737488 64.559072 65.908557 73.304601 82.616101 81.113216 72.032652 72.128976 70.421851 64.545350
ENSMUSG00000000134 33.480962 36.683455 38.983454 39.609181 45.133664 47.577079 38.693647 19.425951 20.279171 18.258503 19.466904 20.759852 19.514517 20.453484 23.945999 24.145265 23.148770 24.341792 20.775309 26.254515 25.201055 25.298568 24.448624 70.194375 70.469434 72.039951 62.710030 65.426174 62.960782 52.892113
ENSMUSG00000000142 34.556890 33.051268 31.249676 26.910897 25.255541 26.237510 24.678957 4.314812 3.851946 5.201375 4.297327 3.944854 3.709407 4.698792 4.180144 4.008057 4.408832 5.413859 4.183791 12.235464 12.443578 13.543412 12.406199 5.611935 4.639913 5.140094 3.517411 3.338406 3.652103 5.866998
ENSMUSG00000000148 12.975017 12.265602 13.479207 15.929561 15.998245 15.251781 16.439377 11.187122 11.765327 12.255481 11.805331 9.727571 10.811203 10.842452 9.828969 9.057784 8.261829 8.418036 8.912263 13.212425 11.883479 13.518251 13.482611 10.102323 10.474313 9.593967 7.725926 8.271340 8.221738 8.493223
ENSMUSG00000000149 62.786926 60.575599 64.868495 81.035249 77.612600 80.007932 79.501053 19.769167 19.068923 18.725772 19.826181 17.829466 16.300954 17.513712 15.040879 14.602690 13.900191 14.489153 13.408245 37.005858 35.055680 36.987634 36.668932 36.844849 38.723659 38.975206 27.466288 29.294849 29.622090 30.355845
ENSMUSG00000000154 0.333055 0.115844 0.432376 0.325385 0.332173 0.555266 0.274624 0.315355 0.635393 0.370886 0.117576 0.114637 0.156711 0.361648 0.316280 0.277836 0.297838 0.396606 0.245718 0.372255 0.472991 0.428795 0.411684 1.678056 1.611784 1.901671 3.967049 5.087375 4.540421 5.049930
ENSMUSG00000000157 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.019660 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.038957 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
ENSMUSG00000000159 0.000000 0.000000 0.000000 0.000000 0.066624 0.000000 0.000000 0.000000 0.000000 0.120014 0.192008 0.000000 0.069363 0.465487 0.133256 0.105527 0.609742 0.232339 0.000000 0.000000 0.000000 0.000000 0.034548 0.047125 0.116238 0.039351 0.258603 0.235119 0.181094 0.033496
ENSMUSG00000000167 7.167658 7.111275 4.562918 4.611452 5.165249 3.980233 5.810633 3.014607 2.165885 1.420868 2.402707 1.174127 1.731610 2.442734 1.718371 1.983554 1.487518 0.867104 1.640726 2.877345 4.086346 2.980909 2.788065 2.412113 1.093266 2.134765 3.075103 2.544267 4.048963 1.194254
ENSMUSG00000000168 46.898364 51.544129 50.803506 46.728899 46.416810 43.668901 42.770089 135.597979 142.174183 140.466776 155.267598 176.558772 177.449941 173.533809 177.441385 140.085372 135.955772 149.522280 139.289347 77.093701 65.875673 72.031057 69.113490 65.284363 59.589716 67.122029 66.846252 65.352465 66.011315 60.027737
ENSMUSG00000000171 183.510028 191.339737 185.366419 146.485489 136.055305 134.774362 144.419003 250.624457 258.445965 254.599788 264.936389 297.223823 290.955520 287.995065 284.714197 228.171116 236.310898 224.511224 221.322408 180.849179 173.186896 175.786723 175.691677 147.487631 156.118401 153.095971 219.561253 219.763695 210.634119 196.601985
ENSMUSG00000000182 0.103193 0.032365 0.029772 0.000000 0.000000 0.000000 0.000000 0.109766 0.257429 0.112760 0.211044 0.082172 0.037902 0.110168 0.111682 0.380279 0.139486 0.081007 0.246593 0.169495 0.356141 0.264438 0.269860 0.040837 0.041344 0.036939 0.000000 0.030240 0.000000 0.203803
ENSMUSG00000000183 0.000000 0.000000 0.010340 0.000000 0.010401 0.016126 0.000000 0.000000 0.000000 0.000000 0.015047 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.012499 0.000000 0.000000 0.000000 0.000000 0.012861 0.000000 0.000000 0.000000 0.020984
ENSMUSG00000000184 142.747177 147.826378 149.197724 66.761615 63.335841 65.070470 48.113890 21.348042 17.384228 15.935255 15.653769 16.292530 14.504585 17.797905 15.544638 17.701942 13.309930 15.605609 17.667904 28.101151 33.067019 38.044163 34.866641 7.046037 5.154655 5.428973 9.002990 11.536859 8.437719 2.867368
ENSMUSG00000000194 36.103417 38.818908 42.800890 36.817530 38.667199 36.778395 26.187279 33.002131 37.727627 36.606405 37.474231 36.703396 34.536615 36.214178 34.713799 33.020334 26.550992 28.899042 31.118602 34.300556 32.621947 33.363838 34.012681 30.084083 26.911440 26.198168 28.291703 30.043730 28.726287 29.337298
ENSMUSG00000000197 0.724701 0.864508 0.538785 1.073368 1.074458 0.792837 0.844630 25.556904 27.172721 27.223684 27.307364 23.469370 25.193771 24.809004 25.595933 22.134882 17.538889 21.238627 22.714445 11.528675 11.227085 11.667388 11.717720 0.717834 0.544315 0.843883 0.333168 0.479806 0.295036 0.135287
ENSMUSG00000000202 46.873410 38.860603 20.595529 21.277139 19.304139 22.041307 20.637502 1.998099 2.664860 2.147623 1.671951 0.707986 1.171352 2.805967 0.762410 1.275708 0.275591 0.391964 0.762712 3.703893 4.510447 4.937764 4.553973 1.186139 1.366082 0.695439 1.176641 2.441639 0.983961 3.130113
ENSMUSG00000000204 0.000000 0.077812 0.000000 0.036758 0.087585 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.043964 0.000000 0.000000 0.000000 0.018414 0.000000 0.000000 0.000000 0.038316 0.000000 0.000000 0.050157 0.000000 0.041803 0.000000 0.040884 0.152281
ENSMUSG00000000214 0.316821 0.376734 0.580427 0.074393 0.077525 0.232658 0.047076 0.000000 0.000000 0.000000 0.195838 0.050855 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.059494 0.028719 0.000000 0.080425 0.133600 0.051463 0.000000 0.062615 0.191165 0.206326 0.128695 0.070217
ENSMUSG00000000215 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.201293 0.000000 0.000000 0.079614 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.125601 0.000000 0.060125 0.339217 0.000000 0.526094 0.000000 0.000000 0.000000 0.000000 0.000000
ENSMUSG00000000216 0.055074 0.051534 0.015706 0.000000 0.031705 0.000000 0.000000 0.039904 0.022940 0.019738 0.000000 0.000000 0.020080 0.000000 0.000000 0.000000 0.018491 0.000000 0.038279 0.017680 0.000000 0.017147 0.017771 0.022180 0.000000 0.000000 0.000000 0.065627 0.033124 0.015572
ENSMUSG00000000223 1.490207 1.396739 1.636890 1.736106 1.971587 1.840778 1.365031 2.454484 2.567922 2.499309 2.498017 2.092696 1.983752 2.066465 1.717463 2.653865 1.954384 1.841953 2.723702 2.868183 2.988016 3.715324 3.097974 0.185306 0.237818 0.133284 0.229376 0.237803 0.222297 0.190730
ENSMUSG00000000244 0.083692 0.054689 0.130041 0.090199 0.193026 0.120789 0.083430 0.218939 0.038914 0.099444 0.015106 0.000000 0.000000 0.013459 0.000000 0.154390 0.067568 0.270073 0.127634 0.108335 0.113842 0.088430 0.052551 0.638876 0.595938 0.471887 0.533838 0.596377 0.453464 0.350619
ENSMUSG00000000247 3.581850 3.499372 3.528227 2.710486 3.101592 2.767801 2.565610 1.225779 0.659762 1.060458 0.978868 0.964519 0.809062 0.716354 0.546474 0.468901 0.592905 0.252756 0.383217 9.739157 11.367096 12.204956 9.050891 1.025635 1.058490 0.835800 0.563193 0.906406 0.423688 0.336354
ENSMUSG00000000248 0.000000 0.307460 0.275004 0.000000 0.000000 0.037225 0.000000 0.000000 0.000000 0.000000 0.000000 0.726755 0.000000 0.000000 0.000000 0.000000 0.000000 0.032926 0.094631 0.027475 0.000000 0.264993 0.027614 0.000000 0.035735 0.000000 0.000000 0.000000 0.000000 0.000000
ENSMUSG00000000253 5.002777 4.525335 4.179621 5.048770 4.947710 4.666987 4.505384 23.658215 24.517931 25.751619 26.217720 35.486540 36.812237 35.029205 34.464815 47.977399 51.307180 44.104269 43.421043 18.585852 19.591512 21.542503 19.528295 10.439729 10.042434 12.184047 20.222381 19.607781 19.986088 25.072968
ENSMUSG00000000263 2.981638 1.970632 2.113011 1.671629 1.341986 2.387899 0.558066 43.069555 53.099363 55.631124 57.681339 85.004519 87.323404 79.087829 83.067736 94.715692 60.793459 96.932514 95.108836 3.780805 3.893267 3.860449 4.027712 0.037352 0.037589 0.032486 0.056548 0.079220 0.058177 0.109525
ENSMUSG00000000266 4.207659 3.886346 3.670926 5.967348 5.313591 5.564873 3.908537 18.434130 19.178063 18.998609 19.210836 22.110790 21.232814 21.788512 20.701333 21.877453 18.154458 24.446192 24.809788 14.273999 14.445732 13.674834 13.532920 0.403795 0.353641 0.384367 0.045010 0.072257 0.094877 0.126511
ENSMUSG00000000275 15.337964 15.667032 22.966957 6.220162 6.831170 6.443813 6.385162 1.564104 1.618634 1.642415 1.574792 1.154512 1.511644 1.017289 0.828477 1.030165 0.983866 0.759897 1.061297 3.119988 3.554271 3.629310 3.486077 44.342670 44.274541 45.952267 68.204400 66.534540 67.546482 65.233133
ENSMUSG00000000276 6.707699 6.335487 5.715570 11.190985 11.445650 10.558276 8.857024 23.079722 24.157868 24.513742 24.306458 22.041553 22.124420 21.233254 22.742842 28.209391 23.221775 27.581952 27.546310 20.847164 20.360543 19.165914 19.357199 7.914284 8.048394 8.766309 11.622329 11.302601 11.791095 7.286859
ENSMUSG00000000278 90.617387 96.681487 113.010324 37.035350 35.574970 37.008561 35.981529 53.884610 53.796869 52.070052 55.294585 71.168345 71.952532 69.563003 72.057016 80.149296 90.857958 89.634870 77.269610 38.043631 38.898064 39.409246 38.148701 174.002655 172.278617 171.035085 162.763884 161.548267 159.085443 194.152958
ENSMUSG00000000282 28.352789 28.584065 28.490442 51.536750 48.043432 53.508818 41.034434 34.048141 39.276596 38.718342 42.472822 33.504992 34.355608 38.392612 31.313721 28.164786 28.592877 31.653213 29.223527 46.532177 45.670087 48.495228 50.032227 27.125277 25.598833 24.739579 18.692252 16.789268 15.102999 17.659619

TPM as an expression metric

Alright, not bad!

Let’s stop and think for a minute about what tximport did and the metric we are using (TPM). What does transcripts per million mean? Well, it means pretty much what it sounds like. For every million transcripts in the cell, X of them are this particular transcript. Importantly, this means when this TPM value was calculated from the number of counts a transcript received, this number had to be adjusted for both the total number of counts in the library and the length of a transcript.

If sample A had twice the number of total counts as sample B (i.e. was sequenced twice as deeply), then you would expect every transcript to have approximately twice the number of counts in sample A as it has in sample B. Similarly, if transcript X is twice as long as transcript Y, then you would expect that if they were equally expressed (i.e. the same number of transcript X and transcript Y molecules were present in the sample) that X would have approximately twice the counts that Y does. Working with expression units of TPM incorporates both of these normalizations.

So, if a TPM of X means that for every million transcripts in the sample that X of them were the transcript of interest, then the sum of TPM values across all species should equal one million, right?

Let’s check and see if that’s true.

TPM as an expression metric

sum(tpms$DIVminus8.Rep1)
[1] 985331.8
sum(tpms$DIVminus8.Rep2)
[1] 984957.6
sum(tpms$DIVminus8.Rep3)
[1] 984765.1

OK, not quite one million, but pretty darn close.

This notion that TPMs represent proportions of a whole also leads to another interesting insight into what tximport is doing here. If all transcripts belong to genes, then the TPM for a gene must be the sum of the TPMs of its transcripts. Can we verify that that is true?

TPM as an expression metric

# Redefine for clarity in comparisons
tpms.genes <- tpms

# Make a new tximport object, but this time instead
# of giving gene expression values, give transcript expression values
# This is controlled by the `txOut` argument
txi.transcripts <- tximport(
  salmdir,
  type = "salmon",
  tx2gene = t2g,
  dropInfReps = TRUE,
  countsFromAbundance = "lengthScaledTPM",
  txOut = TRUE
)
reading in files with read_tsv
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
# Make a table of tpm values for every transcript
tpms.txs <- as.data.frame(txi.transcripts$abundance) |>
  set_names(rownames(metadata)) |>
  rownames_to_column(var = "ensembl_transcript_id") |>
  inner_join(t2g, ., by = "ensembl_transcript_id")

gt(tpms.txs[1:20, ])
ensembl_transcript_id DIV0.Rep1 DIV0.Rep2 DIV0.Rep3 DIV1.Rep1 DIV1.Rep2 DIV1.Rep3 DIV1.Rep4 DIV16.Rep1 DIV16.Rep2 DIV16.Rep3 DIV16.Rep4 DIV21.Rep1 DIV21.Rep2 DIV21.Rep3 DIV21.Rep4 DIV28.Rep1 DIV28.Rep2 DIV28.Rep3 DIV28.Rep4 DIV7.Rep1 DIV7.Rep2 DIV7.Rep3 DIV7.Rep4 DIVminus4.Rep1 DIVminus4.Rep2 DIVminus4.Rep3 DIVminus8.Rep1 DIVminus8.Rep2 DIVminus8.Rep3 DIVminus8.Rep4 ensembl_gene_id
ENSMUST00000119854 2.286511 2.475351 2.778492 3.752931 4.290970 3.606815 2.289835 4.753009 6.407206 6.093523 8.073422 4.114218 3.854100 4.504607 4.233634 0.000000 3.062028 2.657892 4.551532 7.708040 5.771621 4.913897 5.733164 0.821894 0.000000 0.791545 0.452225 0.283685 0.253438 0.000000 ENSMUSG00000037736
ENSMUST00000147408 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ENSMUSG00000052516
ENSMUST00000070080 17.294246 16.642412 20.145840 22.849371 23.111531 23.055949 18.922899 85.742059 92.095921 92.061439 90.117232 80.253664 82.962866 82.837521 83.218155 68.286276 59.179619 65.655743 67.027119 61.090866 60.764495 60.258015 58.545413 22.716225 23.042382 25.299848 18.289952 19.497363 17.597384 22.126199 ENSMUSG00000056124
ENSMUST00000033908 0.277638 0.771701 0.734236 0.835938 1.130580 1.774933 0.262763 0.231432 0.209738 0.302866 0.342360 0.394527 0.196355 0.088545 0.170725 0.088308 0.148182 0.037937 0.361527 0.124041 0.411704 0.216135 0.384765 0.627833 0.412680 0.393967 0.177899 0.097658 0.091573 0.404344 ENSMUSG00000031511
ENSMUST00000174016 8.545914 6.122679 4.978280 13.429257 13.448801 13.789999 9.139241 3.772152 6.493719 6.454788 7.620481 3.964981 3.769810 4.272302 4.711036 4.523138 4.025177 6.303732 5.551957 20.404943 18.886851 26.036222 24.703049 15.936284 17.060844 18.859078 9.623711 12.255722 10.591347 2.846295 ENSMUSG00000036036
ENSMUST00000082868 5.240588 7.435392 21.577246 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 3.157229 8.363363 2.106274 7.631468 4.013754 0.000000 ENSMUSG00000064802
ENSMUST00000187399 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.112263 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ENSMUSG00000090925
ENSMUST00000159512 1.197789 1.783770 1.592837 3.586005 2.904509 3.830286 1.389749 1.361614 1.953179 1.801017 2.061461 2.504494 1.787667 1.311947 1.985731 2.389696 0.801072 1.741500 1.538186 0.880757 2.094465 0.624705 1.609800 0.248865 0.954488 1.411125 1.029973 0.838168 0.218119 0.135338 ENSMUSG00000029207
ENSMUST00000194793 0.000000 0.068955 0.061078 0.064820 0.000000 0.000000 0.000000 0.078959 0.177114 0.299510 0.089493 0.181351 0.080516 0.310951 0.222825 0.324322 0.070346 0.163243 0.468088 0.067476 0.360582 0.332645 0.068630 0.000000 0.000000 0.000000 0.155780 0.076557 0.000000 0.000000 ENSMUSG00000103497
ENSMUST00000221298 0.077941 0.100916 0.090854 0.309668 0.244962 0.239357 0.299259 0.174734 0.291636 0.193253 0.231279 0.296867 0.117060 0.200170 0.109928 0.266300 0.103878 0.301950 0.395391 0.569643 0.215417 0.462677 0.353049 0.000000 0.000000 0.028680 0.000000 0.000000 0.000000 0.050635 ENSMUSG00000113688
ENSMUST00000138551 0.000000 0.000000 0.119676 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ENSMUSG00000034164
ENSMUST00000210944 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ENSMUSG00000110312
ENSMUST00000194146 0.066535 0.185273 0.055779 0.000000 0.000000 0.000000 0.070382 0.000000 0.000000 0.000000 0.078488 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.129887 0.000000 0.000000 0.000000 0.162839 0.143064 0.000000 0.000000 0.058096 0.000000 ENSMUSG00000102375
ENSMUST00000112725 1.270589 1.049486 1.271776 1.294794 1.619005 1.511612 0.893824 0.688353 0.884466 0.635204 0.709094 0.928998 0.846144 0.829175 0.696928 0.989624 1.018670 0.818393 1.100337 1.270363 0.878781 1.156467 1.281586 1.667594 1.633202 1.868757 1.477511 1.274160 1.640671 1.306233 ENSMUSG00000025269
ENSMUST00000181391 2.222061 1.416381 2.497798 3.006018 3.142753 1.571165 3.167750 3.530355 2.965603 4.649879 3.558337 3.465148 3.203126 3.106205 2.862499 0.000000 1.333758 2.039999 3.282519 5.710520 3.937353 6.847600 3.991743 1.733049 0.496494 1.601051 1.270107 1.507055 1.477476 0.000000 ENSMUSG00000030446
ENSMUST00000223290 0.000000 0.000000 0.000000 0.026984 0.000000 0.000000 0.032844 0.033378 0.000000 0.000000 0.000000 0.000000 0.000000 0.033128 0.000000 0.000000 0.031158 0.072496 0.032133 0.000000 0.031972 0.029910 0.000000 0.000000 0.036096 0.000000 0.029574 0.000000 0.000000 0.000000 ENSMUSG00000035954
ENSMUST00000116191 0.000000 0.044746 0.000000 0.000000 0.104528 0.168647 0.046020 0.048814 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.102273 0.042109 0.000000 0.069164 0.000000 0.111104 0.170086 0.000000 0.000000 ENSMUSG00000080078
ENSMUST00000060782 0.267962 0.170311 0.391316 0.446440 0.000000 0.000000 0.389636 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.087233 0.046855 0.000000 0.000000 0.110542 0.000000 0.048208 0.044187 0.083067 0.328807 0.000000 ENSMUSG00000051716
ENSMUST00000141479 0.960027 0.541858 0.000000 0.000000 0.473587 1.080745 0.000000 0.274066 0.000000 0.000000 0.000000 0.000000 0.359059 1.129914 0.300957 0.388096 0.000000 0.000000 0.801308 1.141492 1.285565 0.314952 1.575395 0.000000 0.348874 0.398863 0.000000 0.970236 1.421541 0.351342 ENSMUSG00000049119
ENSMUST00000211096 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ENSMUSG00000110353

OK so lets look at the expression of ENSMUSG00000020634 in the first sample (DIVminus8.Rep1).

TPM as an expression metric

# Get sum of tpm values for transcripts that belong to ENSMUSG00000020634
tpms.tx.ENSMUSG00000020634 <- filter(
  tpms.txs,
  ensembl_gene_id == "ENSMUSG00000020634"
)

sumoftxtpm <- sum(tpms.tx.ENSMUSG00000020634$DIVminus8.Rep1)

# Get gene level tpm value of ENSMUSG00000020634
genetpm <- filter(
  tpms.genes,
  ensembl_gene_id == "ENSMUSG00000020634"
) |>
  pull(DIVminus8.Rep1)

# Are they the same?
sumoftxtpm
[1] 46.71414
genetpm
[1] 46.71414

Basic RNAseq QC

OK now that we’ve got expression values for all genes, we now might want to use these expression values to learn a little bit about our samples. One simple question is > Are replicates similar to each other, or at least more similar to each other than to other samples?

If our data is worth anything at all, we would hope that differences between replicates, which are supposed to be drawn from the same condition, are smaller than differences between samples drawn from different conditions. If that’s not true, it could indicate that one replicate is very different from other replciates (in which case we might want to remove it), or that the data in general is of poor quality.

Another question is:

How similar is each sample to every other sample?

In our timecourse, we might expect that samples drawn from adjacent timepoints might be more similar to each other than samples from more distant timepoints.

Hierarchical clustering

A simple way to think about this is to simply correlate TPM values for genes between samples. For plotting purposes here, let’s plot the log(TPM) of two samples against each other. However, for the actual correlation coefficient we are going to be using the Spearman correlation method, which uses ranks, not absolute values. This means that whether or not you take the log will have no effect on the Spearman correlation coefficient.

# DIVminus8.Rep1 vs DIVminus8.Rep2

# Since we are plotting log TPM values, we need to add a pseudocount to all samples.
# log(0) is a problem.

# Add pseudocounts and take log within ggplot function call
r.spearman <- cor.test(
  tpms$DIVminus8.Rep1,
  tpms$DIVminus8.Rep2,
  method = "spearman"
) |>
  broom::tidy() |>
  pull(estimate)

Hierarchical clustering

Warning in cor.test.default(tpms$DIVminus8.Rep1,
tpms$DIVminus8.Rep2, method = "spearman"): Cannot compute
exact p-value with ties
r.spearman <- signif(r.spearman, 2)

ggplot(
  tpms,
  aes(x = log10(DIVminus8.Rep1 + 1e-3), y = log10(DIVminus8.Rep2 + 1e-3))
) +
  geom_point() +
  theme_classic() +
  annotate("text", x = 2, y = 0, label = paste0("R = ", r.spearman))

Scatter plot comparing log10 TPM values between two DIV-8 replicates, annotated with the Spearman correlation coefficient.

Hierarchical clustering

With RNAseq data, the variance of a gene’s expression increases as the expression increases. However, using a pseudocount and taking the log of the expression value actually reverses this trend. Now, genes with the lowest expression have the most variance. Why is this a problem? Well, the genes with the most variance are going to be the ones that contribute the most to intersample differences. Ideally, we would like to therefore remove the relationship between expression and variance.

There are transformations, notably rlog and vst, that are made to deal with this, but they are best used when dealing with normalized count data, while here we are dealing with TPMs. We will talk about counts later, but not here.

So, for now, we will take another approach of simply using an expression threshold. Any gene that does not meet our threshold will be excluded from the analysis. Obviously where to set this threshold is a bit subjective. For now, we will set this cutoff at 1 TPM.

Hierarchical clustering

# DIVminus8.Rep1 vs DIVminus8.Rep2

# Since we are plotting log TPM values we will only keep for genes that have expression of at least 1 TPM in both samples
tpms.2samplecor <-
  select(tpms, c(ensembl_gene_id, DIVminus8.Rep1, DIVminus8.Rep2)) |>
  filter(DIVminus8.Rep1 >= 1 & DIVminus8.Rep2 >= 1)

# pull the correlation coefficient
r.spearman <- cor.test(
  tpms.2samplecor$DIVminus8.Rep1,
  tpms.2samplecor$DIVminus8.Rep2,
  method = "spearman"
) |>
  broom::tidy() |>
  pull(estimate)

Hierarchical clustering

Warning in cor.test.default(tpms.2samplecor$DIVminus8.Rep1,
tpms.2samplecor$DIVminus8.Rep2, : Cannot compute exact
p-value with ties
# round/set sig digits
r.spearman <- signif(r.spearman, 2)

# plot
ggplot(
  tpms.2samplecor,
  aes(x = log10(DIVminus8.Rep1 + 1e-3), y = log10(DIVminus8.Rep2 + 1e-3))
) +
  geom_point() +
  theme_classic() +
  annotate(
    "text",
    x = 2,
    y = 1,
    label = paste0("R = ", r.spearman)
  )

Scatter plot comparing log10 TPM values between two DIV-8 replicates after filtering to genes with at least 1 TPM in both, annotated with the Spearman correlation coefficient.

Filtering lowly expressed genes

OK that’s two samples compared to each other, but now we want to see how all samples compare to all other samples. Before we do this we need to decide how to apply our expression cutoff across many samples. Should a gene have to meet the cutoff in only one sample? In all samples? Let’s start by saying it has to meet the cutoff in at least half of the 30 samples.

# Make a new column in tpms that is the number of samples in which the value is at least 1
tpms.cutoff <-
  mutate(tpms, nSamples = rowSums(tpms[, 2:31] > 1)) |>
  # Now filter for rows where nSamples is at least 15
  # Meaning that at least 15 samples passed the threshold
  filter(nSamples >= 15) |>
  # Get rid of the nSamples column
  select(-nSamples)

nrow(tpms)
[1] 49781
nrow(tpms.cutoff)
[1] 14836

Correlating gene expression values

Now we can use the cor function to calculate pairwise correlations in a .red[matrix] of TPM values.

tpms.cutoff.matrix <-
  select(tpms.cutoff, -ensembl_gene_id) |>
  as.matrix()

tpms.cor <- cor(tpms.cutoff.matrix, method = "spearman")
head(tpms.cor)
          DIV0.Rep1 DIV0.Rep2 DIV0.Rep3 DIV1.Rep1 DIV1.Rep2
DIV0.Rep1 1.0000000 0.9921977 0.9856995 0.9344186 0.9319214
DIV0.Rep2 0.9921977 1.0000000 0.9895483 0.9289543 0.9268475
DIV0.Rep3 0.9856995 0.9895483 1.0000000 0.9171840 0.9148961
DIV1.Rep1 0.9344186 0.9289543 0.9171840 1.0000000 0.9933024
DIV1.Rep2 0.9319214 0.9268475 0.9148961 0.9933024 1.0000000
DIV1.Rep3 0.9317373 0.9269880 0.9160581 0.9913271 0.9911436
          DIV1.Rep3 DIV1.Rep4 DIV16.Rep1 DIV16.Rep2
DIV0.Rep1 0.9317373 0.9340858  0.6671896  0.6541035
DIV0.Rep2 0.9269880 0.9300994  0.6606971  0.6458159
DIV0.Rep3 0.9160581 0.9174434  0.6372218  0.6209717
DIV1.Rep1 0.9913271 0.9857919  0.7546652  0.7418435
DIV1.Rep2 0.9911436 0.9851766  0.7565788  0.7436876
DIV1.Rep3 1.0000000 0.9863776  0.7559111  0.7418927
          DIV16.Rep3 DIV16.Rep4 DIV21.Rep1 DIV21.Rep2
DIV0.Rep1  0.6547713  0.6555622  0.6428668  0.6436133
DIV0.Rep2  0.6474671  0.6477460  0.6363783  0.6367204
DIV0.Rep3  0.6221840  0.6225424  0.6120404  0.6132091
DIV1.Rep1  0.7431686  0.7435523  0.7233850  0.7234503
DIV1.Rep2  0.7455848  0.7455058  0.7251078  0.7252111
DIV1.Rep3  0.7443106  0.7440814  0.7240321  0.7238090
          DIV21.Rep3 DIV21.Rep4 DIV28.Rep1 DIV28.Rep2
DIV0.Rep1  0.6454079  0.6420882  0.6383834  0.6415068
DIV0.Rep2  0.6385565  0.6338002  0.6310968  0.6346436
DIV0.Rep3  0.6137075  0.6090538  0.6079274  0.6111133
DIV1.Rep1  0.7258146  0.7219970  0.7161735  0.7138980
DIV1.Rep2  0.7276872  0.7245541  0.7181366  0.7164069
DIV1.Rep3  0.7259111  0.7221164  0.7164469  0.7139429
          DIV28.Rep3 DIV28.Rep4 DIV7.Rep1 DIV7.Rep2
DIV0.Rep1  0.6334989  0.6350530 0.7736803 0.7786601
DIV0.Rep2  0.6257407  0.6281032 0.7665588 0.7717310
DIV0.Rep3  0.6024965  0.6045994 0.7429511 0.7488361
DIV1.Rep1  0.7102763  0.7128705 0.8500321 0.8517566
DIV1.Rep2  0.7129282  0.7150473 0.8518114 0.8529265
DIV1.Rep3  0.7109473  0.7128181 0.8502310 0.8518734
          DIV7.Rep3 DIV7.Rep4 DIVminus4.Rep1 DIVminus4.Rep2
DIV0.Rep1 0.7765741 0.7751908      0.8312509      0.8284210
DIV0.Rep2 0.7693845 0.7687242      0.8355661      0.8330637
DIV0.Rep3 0.7460371 0.7450699      0.8513941      0.8499737
DIV1.Rep1 0.8518416 0.8512450      0.7364004      0.7346787
DIV1.Rep2 0.8533955 0.8520150      0.7327968      0.7301669
DIV1.Rep3 0.8522113 0.8518847      0.7339962      0.7312978
          DIVminus4.Rep3 DIVminus8.Rep1 DIVminus8.Rep2
DIV0.Rep1      0.8297235      0.7951697      0.7960214
DIV0.Rep2      0.8341773      0.8016991      0.8025049
DIV0.Rep3      0.8515894      0.8181586      0.8185187
DIV1.Rep1      0.7352999      0.6985884      0.6992813
DIV1.Rep2      0.7318231      0.6953795      0.6958153
DIV1.Rep3      0.7337089      0.6966838      0.6970009
          DIVminus8.Rep3 DIVminus8.Rep4
DIV0.Rep1      0.7958163      0.7849284
DIV0.Rep2      0.8009424      0.7898661
DIV0.Rep3      0.8187167      0.8067762
DIV1.Rep1      0.6995943      0.6906624
DIV1.Rep2      0.6952947      0.6866216
DIV1.Rep3      0.6969669      0.6879696

Hierarchical clustering

Now we need to plot these and have similar samples (i.e. those that are highly correlated with each other) be clustered near to each other. We will use pheatmap to do this.

# let's pull information that we want to add as categories
pheatmap(
  tpms.cor,
  annotation_col = metadata[, 2:3],
  fontsize = 7,
  show_colnames = FALSE
)

This looks pretty good! There are two main points to takeaway here. First, all replicates for a given timepoint are clustering with each other. Second, you can kind of derive the order of the timepoints from the clustering. The biggest separation is between early (DIVminus8 to DIV1) and late (DIV7 to DIV28). After that you can then see finer-grained structure.

Hierarchical clustering

PCA analysis

Another way to visualize relationships is using a dimensionality reduction technique called Principal Component Analysis (PCA). Let’s watch this short video. It focuses more on how to interpret them rather than the math behind their creation.

PCA analysis

PCA works best when values are approximately normally distributed, so we will first take the log of our expression values.

With our cutoff as it is now (genes have to have expression of at least 1 TPM in half the samples), it is possible that we will have some 0 values. Taking the log of 0 might cause a problem, so we will add a pseudocount.

tpms.cutoff.matrix <-
  select(tpms.cutoff, -ensembl_gene_id) |>
  as.matrix()

# Add pseudocount and take log2
tpms.cutoff.matrix <- log2(tpms.cutoff.matrix + 1e-3)

# scale - annoying double transpose
tpms.cutoff.matrix <- t(scale(t(tpms.cutoff.matrix)))

PCA analysis

Very similar interpretation as before (heatmap of correlation).

# prcomp expects samples to be rownames, right now they are columns
# so we need to transpose the matrix using `t`
tpms.pca <- prcomp(t(tpms.cutoff.matrix))

# The coordinates of samples on the principle components are stored in the $x slot
# These are what we are going to use to plot
# We can also also some data about the samples here so that our plot is a little more interesting

### Tricky piping!!
tpms.pca.pc <- as.data.frame(tpms.pca$x) |>
  rownames_to_column(var = "sample_id") |>
  left_join(metadata[, 1:4], by = "sample_id")


# We can see how much of the total variance is explained by each PC using the summary function
tpms.pca.summary <- summary(tpms.pca)$importance

# The amount of variance explained by PC1 is the second row, first column of this table
# It's given as a fraction of 1, so we multiply it by 100 to get a percentage
pc1var <- round(tpms.pca.summary[2, 1] * 100, 1)

# The amount of variance explained by PC2 is the second row, second column of this table
pc2var <- round(tpms.pca.summary[2, 2] * 100, 1)

# Reorder timepoints explicitly for plotting purposes

tpms.pca.pc$samp <-
  factor(
    tpms.pca.pc$samp,
    levels = c(
      "DIVminus8",
      "DIVminus4",
      "DIV0",
      "DIV1",
      "DIV7",
      "DIV16",
      "DIV21",
      "DIV28"
    )
  )

# Plot results
ggplot(
  data = tpms.pca.pc,
  aes(
    x = PC1,
    y = PC2,
    color = samp,
    label = sample_id
  )
) +
  geom_point(size = 5) +
  scale_color_brewer(palette = "Set1") +
  theme_cowplot(16) +
  labs(
    x = paste("PC1,", pc1var, "% explained var."),
    y = paste("PC2,", pc2var, "% explained var.")
  ) +
  geom_text_repel()