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

[DESCRIPTION]
This protocol is for comparing 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 differential coverage, 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):


 


SECTION: Transcript Index Preparation
2.1. If using LAST

Following Martin Frith's recommendation, the '-uNEAR' seeding scheme is used to slightly increase sensitivity. 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: Transcript Index Preparation
2.2. If using minimap2

A reference index is generated with the -d command-line argument:

 

Note that minimap2 doesn't allow adjustment of alignment scores to have different penalties for insertions and deletions, or different base changes, and this could affect the mappability of reads. Nanopore basecalling has a higher likelihood of mis-calling transitions over transversions (presumably due to a more similar electrochemical structure), and slightly favours deletions (i.e. signal loss) over insertions (i.e. signal misinterpretations).
SECTION: Transcriptome Mapping
3. Reads are mapped to the transcriptome to create an output associating each read with a transcript. 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 transcripts in this process, 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. If using LAST

Reads are mapped to the transcriptome using LAST. The results of that mapping can be piped through last-split and 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, only retaining hits that are either at least 60% of the query (read) length, or at least 75% of the target length.

LAST reports multiple hits per read (including multiple hits for each read/transcript pair), so this is further ordered by the gap-compressed identity colum of the CSV output using sort, then filtered again by sort to only retain the first highest accuracy hit per read. 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
3.2. If using minimap2

Reads are mapped to the transcriptome using minimap2. The results of that mapping (as PAF format) are further processed to make sure that there is only one mapping per transcript-read pair, finally converting to a read/transcript/direction space-separated output:


 

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

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.