TITLE: Stranded Transcript Count Table Generation from Long Reads
AUTHORS: David A Eccles

[DESCRIPTION]
This protocol is for generating count tables for different samples at the transcript level, using long reads that are mapped to transcripts.
 
Input(s): demultiplexed and oriented fastq files (see protocol Preparing Reads for Stranded Mapping), transcript reference fasta file, annotation file
 
Output(s): transcript table, sorted by summed coverage across all samples, annotated with gene name / description / location

[BEFORE_START]
Obtain a transcript fasta file, and an annotation file. For the mouse genome, I use the following files:
 
Comprehensive transcript sequences from GENCODE; this should be the union of cDNA, CDS, and ncRNA sequences, usually the first file in the 'FASTA Files' section, named 'Transcript sequences'.
Annotation file obtained from Ensembl BioMart (Ensembl Genes -> Mouse Genes) as a compressed TSV file with the following attribute columns from the 'Features' section: 

 Transcript stable ID
 Gene name
 Gene description
 Chromosome/scaffold name
 Gene start (bp)
 Gene end (bp)
 Strand

A recent version of these files can be obtained from This Zenodo Repository

[STEPS]
SECTION: Demultiplex Reads
1. 
Demultiplex and orient reads as per the protocol Preparing Reads for Stranded Mapping. It is expected that these demultiplexed reads will be split up in the current directory, and coupled with a 'barcode_counts.txt' file. If that's the case, the following should work:
 
Example expected output:
 
If the 'barcode_counts.txt' file is not present, this error will appear:
 
If one or more of the oriented read files is missing, it will look something like this:
 

SECTION: Transcript Index Preparation
2. Prepare transcript index (see Guidelines for data sources).

The GENCODE transcript file is first modified so that the mapped reference sequence name is the first value (i.e. the Transcript id):


 

In order to reduce the risk of polyA and polyT sequences matching to the polyA tail of reads (e.g. as found in Comt-204 / ENSMUST00000165430.8), any A or T homopolymers are masked out with Ns:


 

A temporary shell variable is created to reference the transcript file location:


 

SECTION: Transcript Index Preparation
2.1. Newer versions of LAST (v1409+) include a new seeding scheme, '-uRY4' [and other related RYX schemes], which improves mapping accuracy and reduces polyA matches; low-complexity regions are also converted to lower case. This will generate seven additional files of the form <index name>.XXX:

 

Prepare a substitution matrix for transcript mapping. The default substitution matrix is swayed too much by INDELs in the barcode sequences, so here's one that I've developed using last-train with a recent nanopore cDNA sequencing run:

 
  

The variant matrix here was generated using last-train on one of the read files from a recent cDNA run, called using super-accuracy basecalling with guppy v5.1.15:
 

 


[note: this is a different matrix from that used for demultiplexing and read orientation]
SECTION: Transcriptome Mapping
3. Reads are mapped to the transcriptome to create an output associating each read to one or more transcripts. The outcome of this process is a tab-separated file containing barcode/transcript/read/direction quads (in that order). Reads may be mapped to multiple gene transcripts in this process (e.g. for polycistronic transcripts), which is fine as long as the total sum of mapped transcript/read pairs doesn't play a role in subsequent downstream normalisation.

 

SECTION: Transcriptome Mapping
3.1. Reads are mapped to the transcriptome using LAST, splitting reads into pieces where appropriate for polycistronic transcripts. The results of that mapping are piped through last-postmask to exclude unlikely hits.

Following this, one of my scripts, maf2csv.pl, is used to convert to a one-line-per-mapping CSV format. Based on an eyeballing of mapped results (see here), hits are additionally filtered to exclude 100% matches (these tend to be short, low-complexity subsequences), as well as any mappings shorter than 50% of the read where the proportion of the query that is mapped is less than the proportion of the target.

LAST reports multiple hits per read (including multiple hits for each read/transcript/direction triplet), so this is further ordered by the gap-compressed identity column of the CSV output and unique triplets preserved using sort for easier downstream processing. This intermediate CSV file is preserved for additional read-level mapping and accuracy QC, if desired.

The CSV file is finally converted by awk to a space-separated format to retain only the transcript, read, and direction.
 

SECTION: Transcriptome Mapping
4. The result is then aggregated to sum up counts per transcript/direction pair:
 

Note: I've split this up into two steps so that an intermediate count of the total number of mapped transcripts per barcode can be done:

 

SECTION: Annotation and Result generation
5.   
Transcript counts are merged with ensembl gene annotation, then converted into wide format (one line per transcript) using an R script.
 
The transcript annotation in this case is from ensembl BioMart (see Guidelines for more details).
 

#!/usr/bin/Rscript  library(dplyr); library(tidyr);  ## load ensemble transcript metadata (including gene name) ensembl.df <- as.tbl(read.delim('ensembl_mm10_geneFeatureLocations.txt.gz',                          col.names=c('transcript','Description','Start','End',                                      'Strand','Gene','Chr'),                          stringsAsFactors=FALSE)); ensembl.df$Description <- sub(' \\[.*$','',ensembl.df$Description); ensembl.df$Description <- sub('^(.{50}).+$','\\1...',ensembl.df$Description); ensembl.df[,1:7] <- ensembl.df[,c(1,7,5,3,4,2,6)]; colnames(ensembl.df)[1:7] <- colnames(ensembl.df)[c(1,7,5,3,4,2,6)]; options(scipen=15); ## don't show scientific notation for large positions  ## load used barcode identifiers bcNames <- readLines('used_barcodes.txt');  ## load count data into 'narrow' array (one line per count) trn.counts <- tibble(); for(bc in bcNames){     trn.counts <-         bind_rows(trn.counts,                   as.tbl(read.table(                       sprintf('mapped/trnCounts_LAST_%s_vs_Mmus_transcriptome.txt.gz', bc),                       col.names=c('count','barcode','transcript','dir'),                       stringsAsFactors=FALSE))); }  ## remove revision number from transcript names (if present) trn.counts$transcript <- sub('\\.[0-9]+$','',trn.counts$transcript);  ## convert to wide format (one line per transcript) trn.counts.wide <- spread(trn.counts, barcode, count) %>%     mutate(dir = c('+'='fwd', '-'='rev')[dir]); for(bd in colnames(trn.counts.wide[,-1])){     trn.counts.wide[[bd]] <- replace_na(trn.counts.wide[[bd]],0); }  ## merge ensembl metadata with transcript counts gene.counts.wide <- inner_join(ensembl.df, trn.counts.wide, by='transcript'); gene.counts.wide <- gene.counts.wide[order(-rowSums(gene.counts.wide[,-(1:8)])),];  ## write result out to a file write.csv(gene.counts.wide, file='wide_transcript_counts_LAST.csv', row.names=FALSE);


SECTION: Downstream Workflows
6. Here is a downstream workflow that carries out transcript-level differential expression analysis using DESeq2:

Creating Differential Transcript Expression Results with DESeq2

I would like to emphasise that batch effects should be considered for nanopore sequencing, given how frequently the technology changes. Make sure that at least the sequencing library (i.e. samples prepared in tandem on the same day from the same kit) is added into the statistical model, and try to make sure that sequencing libraries are fairly heterogeneous - replicates from a sample with skewed transcript distributions could influence the outcome of statistical tests.