Skip to content

Instantly share code, notes, and snippets.

@BenLangmead
Created January 11, 2014 20:31
Show Gist options
  • Save BenLangmead/8376306 to your computer and use it in GitHub Desktop.
Save BenLangmead/8376306 to your computer and use it in GitHub Desktop.
{
"metadata": {
"name": ""
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"FASTQ\n",
"=====\n",
"\n",
"This notebook explores [FASTQ], the most common format for storing sequencing reads.\n",
"\n",
"FASTA and FASTQ are rather similar, but FASTQ is almost always used for storing *sequencing reads* (with associated quality values), whereas FASTA is used for storing all kinds of DNA, RNA or protein sequencines (without associated quality values).\n",
"\n",
"Before delving into the format, I should mention that there are great tools and libraries for parsing and manipulating FASTQ, e.g. [FASTX], and [BioPython]'s [SeqIO] module. If your needs are relatively simple, you might try using these tools and libraries and skip reading this document.\n",
"\n",
"[FASTA]: http://en.wikipedia.org/wiki/FASTA_format\n",
"[FASTQ]: http://en.wikipedia.org/wiki/FASTQ_format\n",
"[BioPython]: http://biopython.org/wiki/Main_Page\n",
"[SeqIO]: http://biopython.org/wiki/SeqIO\n",
"[FASTX]: http://hannonlab.cshl.edu/fastx_toolkit/"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Basic format\n",
"Here's a single sequencing read in FASTQ format:\n",
"\n",
" @ERR294379.100739024 HS24_09441:8:2203:17450:94030#42/1\n",
" AGGGAGTCCACAGCACAGTCCAGACTCCCACCAGTTCTGACGAAATGATGAGAGCTCAGAAGTAACAGTTGCTTTCAGTCCCATAAAAACAGTCCTACAA\n",
" +\n",
" BDDEEF?FGFFFHGFFHHGHGGHCH@GHHHGFAHEGFEHGEFGHCCGGGFEGFGFFDFFHBGDGFHGEFGHFGHGFGFFFEHGGFGGDGHGFEEHFFHGE\n",
" @\n",
"\n",
"It's spread across four lines. The four lines are:\n",
"\n",
"1. \"`@`\" followed by a read name\n",
"2. Nucleotide sequence\n",
"3. \"`+`\", possibly followed by some info, but ignored by virtually all tools\n",
"4. Quality sequence (explained below)\n",
"\n",
"Here is a very simple Python function for parsing file of FASTQ records:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def parse_fastq(fh):\n",
" \"\"\" Parse reads from a FASTQ filehandle. For each read, we\n",
" return a name, nucleotide-string, quality-string triple. \"\"\"\n",
" reads = []\n",
" while True:\n",
" first_line = fh.readline()\n",
" if len(first_line) == 0:\n",
" break # end of file\n",
" name = first_line[1:].rstrip()\n",
" seq = fh.readline().rstrip()\n",
" fh.readline() # ignore line starting with +\n",
" qual = fh.readline().rstrip()\n",
" reads.append((name, seq, qual))\n",
" return reads\n",
"\n",
"fastq_string = '''@ERR294379.100739024 HS24_09441:8:2203:17450:94030#42/1\n",
"AGGGAGTCCACAGCACAGTCCAGACTCCCACCAGTTCTGACGAAATGATG\n",
"+\n",
"BDDEEF?FGFFFHGFFHHGHGGHCH@GHHHGFAHEGFEHGEFGHCCGGGF\n",
"@ERR294379.136275489 HS24_09441:8:2311:1917:99340#42/1\n",
"CTTAAGTATTTTGAAAGTTAACATAAGTTATTCTCAGAGAGACTGCTTTT\n",
"+\n",
"@@AHFF?EEDEAF?FEEGEFD?GGFEFGECGE?9H?EEABFAG9@CDGGF\n",
"@ERR294379.97291341 HS24_09441:8:2201:10397:52549#42/1\n",
"GGCTGCCATCAGTGAGCAAGTAAGAATTTGCAGAAATTTATTAGCACACT\n",
"+\n",
"CDAF<FFDEHEFDDFEEFDGDFCHD=GHG<GEDHDGJFHEFFGEFEE@GH'''\n",
"\n",
"from StringIO import StringIO\n",
"\n",
"parse_fastq(StringIO(fastq_string))"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 1
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The nucleotide string can sometimes contain the character \"`N`\". `N` essentially means \"no confidence.\" The sequencer knows there's a nucleotide there but doesn't know whether it's an A, C, G or T."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Read name\n",
"\n",
"Read names often contain information about:\n",
"\n",
"1. The scientific study for which the read was sequenced. E.g. the string `ERR294379` (an [SRA accession number](http://www.ebi.ac.uk/ena/about/sra_format)) in the read names correspond to [this study](http://www.ncbi.nlm.nih.gov/sra/?term=ERR294379).\n",
"2. The sequencing instrument, and the exact *part* of the sequencing instrument, where the DNA was sequenced. See the [FASTQ format](http://en.wikipedia.org/wiki/FASTQ_format#Illumina_sequence_identifiers) wikipedia article for specifics on how the Illumina software encodes this information.\n",
"3. Whether the read is part of a *paired-end read* and, if so, which end it is. Paired-end reads will be discussed further below. The `/1` you see at the end of the read names above indicate the read is the first end from a paired-end read."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Quality values\n",
"\n",
"Quality values are probabilities. Each nucleotide in each sequencing read has an associated quality value. A nucleotide's quality value encodes the probability that the nucleotide was *incorrectly called* by the sequencing instrument and its software. If the nucleotide is `A`, the corresponding quality value encodes the probability that the nucleotide at that position is actually *not* an `A`.\n",
"\n",
"Quality values encoded in two senses: first, the relevant probabilities are rescaled using the Phread scale, which is a negative log scale. In other words if *p* us the probability that the nucleotide was incorrectly called, we encode this as *Q* where:\n",
"\n",
"$$Q = -10 \\cdot \\log_{10}(p)$$\n",
"\n",
"Trying a few examples for *p* and *Q* is instructive. For example, if *Q* = 30, then *p* = 0.001, a 1-in-1000 chance that the nucleotide is wrong. If *Q* = 20, then *p* = 0.01, a 1-in-100 chance. If *Q* = 10, then *p* = 0.1, a 1-in-10 chance. And so on.\n",
"\n",
"Second, scaled quality values are *rounded* to the nearest integer and encoded using [ASCII printable characters](http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters). For example, using the Phred33 encoding (which is by far the most common), a $Q$ of 30 is encoded as the ASCII character with code 33 + 30 = 63, which is \"`?`\". A $Q$ of 20 is encoded as the ASCII character with code 33 + 20 = 53, which is \"`5`\". And so on.\n",
"\n",
"Let's define some relevant Python functions:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def phred33_to_q(qual):\n",
" \"\"\" Turn Phred+33 ASCII-encoded quality into Phred-scaled integer \"\"\"\n",
" return ord(qual)-33\n",
"\n",
"def q_to_phred33(Q):\n",
" \"\"\" Turn Phred-scaled integer into Phred+33 ASCII-encoded quality \"\"\"\n",
" return chr(Q + 33)\n",
"\n",
"def q_to_p(Q):\n",
" \"\"\" Turn Phred-scaled integer into error probability \"\"\"\n",
" return 10.0 ** (-0.1 * Q)\n",
"\n",
"def p_to_q(p):\n",
" \"\"\" Turn error probability into Phred-scaled integer \"\"\"\n",
" import math\n",
" return int(round(-10.0 * math.log10(p)))"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 2
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Here are the examples I discussed above\n",
"\n",
"# Convert Qs into ps\n",
"q_to_p(30), q_to_p(20), q_to_p(10)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 3,
"text": [
"(0.001, 0.01, 0.1)"
]
}
],
"prompt_number": 3
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"p_to_q(0.00011) # note that result is rounded"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 4,
"text": [
"40"
]
}
],
"prompt_number": 4
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"q_to_phred33(30), q_to_phred33(20)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 5,
"text": [
"('?', '5')"
]
}
],
"prompt_number": 5
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To convert an entire string Phred33-encoded quality values into the corresponding *Q* or *p* values, I can do the following:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Take the first read from the small example above\n",
"name, seq, qual = parse_fastq(StringIO(fastq_string))[0]\n",
"q_string = map(phred33_to_q, qual)\n",
"p_string = map(q_to_p, q_string)\n",
"print q_string\n",
"print p_string"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"[33, 35, 35, 36, 36, 37, 30, 37, 38, 37, 37, 37, 39, 38, 37, 37, 39, 39, 38, 39, 38, 38, 39, 34, 39, 31, 38, 39, 39, 39, 38, 37, 32, 39, 36, 38, 37, 36, 39, 38, 36, 37, 38, 39, 34, 34, 38, 38, 38, 37]\n",
"[0.000501187233627272, 0.00031622776601683794, 0.00031622776601683794, 0.00025118864315095795, 0.00025118864315095795, 0.00019952623149688788, 0.001, 0.00019952623149688788, 0.00015848931924611126, 0.00019952623149688788, 0.00019952623149688788, 0.00019952623149688788, 0.0001258925411794166, 0.00015848931924611126, 0.00019952623149688788, 0.00019952623149688788, 0.0001258925411794166, 0.0001258925411794166, 0.00015848931924611126, 0.0001258925411794166, 0.00015848931924611126, 0.00015848931924611126, 0.0001258925411794166, 0.0003981071705534969, 0.0001258925411794166, 0.0007943282347242813, 0.00015848931924611126, 0.0001258925411794166, 0.0001258925411794166, 0.0001258925411794166, 0.00015848931924611126, 0.00019952623149688788, 0.000630957344480193, 0.0001258925411794166, 0.00025118864315095795, 0.00015848931924611126, 0.00019952623149688788, 0.00025118864315095795, 0.0001258925411794166, 0.00015848931924611126, 0.00025118864315095795, 0.00019952623149688788, 0.00015848931924611126, 0.0001258925411794166, 0.0003981071705534969, 0.0003981071705534969, 0.00015848931924611126, 0.00015848931924611126, 0.00015848931924611126, 0.00019952623149688788]\n"
]
}
],
"prompt_number": 6
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You might wonder how the sequencer and its software can *know* the probability that a nucleotide is incorrected called. It really can't -- not exactly, at least. This number is just an estimate. To describe exactly how it's estimated is beyond the scope of this notebook; if you're interested, you can search for academic papers with \"base calling\" in the title. Here's a helpful [video by Rafa Irizarry](http://www.youtube.com/watch?v=eXkjlopwIH4).\n",
"\n",
"A final note: other ways of encoding quality values were proposed and used in the past. For example, Phred64 uses an ASCII offset of 64 instead of 33, and Solexa64 uses \"odds\" instead of the probability *p*. But Phred33 is by far the most common today and you will likely never have to worry about this."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Paired-end reads\n",
"\n",
"Sequencing reads can come in *pairs*. Basically instead of reporting a single snippet of nucleotides from the genome, the sequencer might report a *pair* of snippets that appear *close to each other* in the genome. To accomplish this, the sequencer sequences *both ends* of a longer *fragment* of DNA.\n",
"\n",
"Here is simple Python code that mimicks how the sequencer obtains one paired-end read:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Let's just make a random genome of length 1K\n",
"import random\n",
"random.seed(637485)\n",
"genome = ''.join([random.choice('ACGT') for _ in xrange(1000)])\n",
"genome"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 7,
"text": [
"'AGGTAGGACATTCGTTGGTACCTCGTTTGTTTTCGCTCCTTGTGCCGTTAGGAAAACAGCCGCAGGCGCAATACACTGACGACCATTTCCTGGGGCTTTGTGTACGGCATACATTTCATAGCTCGAACTCCATACTCCGCTCCGATCCATAAACAATTATTACGCAACATGATCGTATCGATGTATACGCACGGCGCATCATACACGCTGGGTATCTGCCTTTGTGTCTAATATCGGAGTGGAGAATCAGCTCCTCAACCAGGTCTCATTAGCGCGAGGCTCTCACTTATCAAAACGGGGCGATCAGCTCGTCGGGTTAAATGGGGTCCGGATATTTTTTCAATGTGTTGAAATACCTCCGTAAGTTAGGTTCGATGACGCAGCGAGGGAATAGTGCGCTCCTAGATAGTGTAATAAAGCTCCCTGTTTTAGTCTAGAGCGTTGTGCGAAAACAACTGAGCGCATTCCATCCGTAGTCCGACTGACAATCCCACTAGGCGGATACAGTAGCCTGGACTCGTTCACAAGCACTCCGCTCCCTCCGTCAAAGCATTTTGGGAATGCTTCCTTACACATCCGTAAGATGATTCTCGAAAATTGATTACTAGCATTGATTCAATTACGAGGCGCCGATATTCGTGAAGCACAATGGCAATAGCCGGATAACGTGGAGAACCCCCATCGCGTGGAATACCGGTTCTGACACTGCAACCGTTGTGCCTATCTTAGAGCCCATGTGCCGATATGCAGAGTCTCGTCTACGACAGCGGACGTCGAACGGAGAAGAGAAATAATGGTCGTCGGGATATAGGGATTGTATTGCAATTGCGTATAAAAGATTAGTGTGAACCGATCCTTTAGCGCTGTGTGTCTCTACACGTATATGAAGATGCCACCCTCGACGCATGGTCCGGCGTTGCGCCTCGCTATTTTCTCGTTCCTGCACCCCATCGACATAGCTACTAAAATGTACCCTAGCATCGGAAGACTCCTTCTCACA'"
]
}
],
"prompt_number": 7
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# The sequencer draws a fragment from the genome of length, say, 250\n",
"offset = random.randint(0, len(genome) - 250)\n",
"fragment = genome[offset:offset+250]\n",
"fragment"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 8,
"text": [
"'CGTAGTCCGACTGACAATCCCACTAGGCGGATACAGTAGCCTGGACTCGTTCACAAGCACTCCGCTCCCTCCGTCAAAGCATTTTGGGAATGCTTCCTTACACATCCGTAAGATGATTCTCGAAAATTGATTACTAGCATTGATTCAATTACGAGGCGCCGATATTCGTGAAGCACAATGGCAATAGCCGGATAACGTGGAGAACCCCCATCGCGTGGAATACCGGTTCTGACACTGCAACCGTTGTGCC'"
]
}
],
"prompt_number": 8
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# Then it reads sequences from either end of the fragment\n",
"end1, end2 = fragment[:75], fragment[-75:]\n",
"end1, end2"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 9,
"text": [
"('CGTAGTCCGACTGACAATCCCACTAGGCGGATACAGTAGCCTGGACTCGTTCACAAGCACTCCGCTCCCTCCGTC',\n",
" 'CAATGGCAATAGCCGGATAACGTGGAGAACCCCCATCGCGTGGAATACCGGTTCTGACACTGCAACCGTTGTGCC')"
]
}
],
"prompt_number": 9
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# And because of how the whole biochemical process works, the\n",
"# second end is always from the opposite strand from the first.\n",
"\n",
"import string\n",
"\n",
"# function for reverse-complementing\n",
"_revcomp_trans = string.maketrans(\"ACGTacgt\", \"TGCAtgca\")\n",
"def reverse_complement(s):\n",
" return s[::-1].translate(_revcomp_trans)\n",
"\n",
"end2 = reverse_complement(end2)\n",
"end1, end2"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 10,
"text": [
"('CGTAGTCCGACTGACAATCCCACTAGGCGGATACAGTAGCCTGGACTCGTTCACAAGCACTCCGCTCCCTCCGTC',\n",
" 'GGCACAACGGTTGCAGTGTCAGAACCGGTATTCCACGCGATGGGGGTTCTCCACGTTATCCGGCTATTGCCATTG')"
]
}
],
"prompt_number": 10
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"FASTQ can be used to store paired-end reads. Say we have 1000 paired-end reads. We should store them in a *pair* of FASTQ files. The first FASTQ file (say, `reads_1.fq`) would contain all of the first ends and the second FASTQ file (say, `reads_2.fq`) would contain all of the second ends. In both files, the ends would appear in corresponding order. That is, the first entry in `reads_1.fq` is paired with the first entry in `reads_2.fq` and so on.\n",
"\n",
"Here is a Python function that parses a pair of files containing paired-end reads."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def parse_paired_fastq(fh1, fh2):\n",
" \"\"\" Parse paired-end reads from a pair of FASTQ filehandles\n",
" For each pair, we return a name, the nucleotide string\n",
" for the first end, the quality string for the first end,\n",
" the nucleotide string for the second end, and the\n",
" quality string for the second end. \"\"\"\n",
" reads = []\n",
" while True:\n",
" first_line_1, first_line_2 = fh1.readline(), fh2.readline()\n",
" if len(first_line_1) == 0:\n",
" break # end of file\n",
" name_1, name_2 = first_line_1[1:].rstrip(), first_line_2[1:].rstrip()\n",
" seq_1, seq_2 = fh1.readline().rstrip(), fh2.readline().rstrip()\n",
" fh1.readline() # ignore line starting with +\n",
" fh2.readline() # ignore line starting with +\n",
" qual_1, qual_2 = fh1.readline().rstrip(), fh2.readline().rstrip()\n",
" reads.append(((name_1, seq_1, qual_1), (name_2, seq_2, qual_2)))\n",
" return reads\n",
"\n",
"fastq_string1 = '''@509.6.64.20524.149722/1\n",
"AGCTCTGGTGACCCATGGGCAGCTGCTAGGGAGCCTTCTCTCCACCCTGA\n",
"+\n",
"HHHHHHHGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHIIHHIHFHHF\n",
"@509.4.62.19231.2763/1\n",
"GTTGATAAGCAAGCATCTCATTTTGTGCATATACCTGGTCTTTCGTATTC\n",
"+\n",
"HHHHHHHHHHHHHHEHHHHHHHHHHHHHHHHHHHHHHHDHHHHHHGHGHH'''\n",
"\n",
"fastq_string2 = '''@509.6.64.20524.149722/2\n",
"TAAGTCAGGATACTTTCCCATATCCCAGCCCTGCTCCNTCTTTAAATAAT\n",
"+\n",
"HHHHHHHHHHHHHHHHHHHH@HHFHHHEFHHHHHHFF#FFFFFFFHHHHH\n",
"@509.4.62.19231.2763/2\n",
"CTCTGCTGGTATGGTTGACGCCGGATTTGAGAATCAANAAGAGCTTACTA\n",
"+\n",
"HHHHHHHHHHHHHHHHHHEHEHHHFHGHHHHHHHH>@#@=44465HHHHH'''\n",
"\n",
"from StringIO import StringIO\n",
"\n",
"parse_paired_fastq(StringIO(fastq_string1), StringIO(fastq_string2))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 11,
"text": [
"[(('509.6.64.20524.149722/1',\n",
" 'AGCTCTGGTGACCCATGGGCAGCTGCTAGGGAGCCTTCTCTCCACCCTGA',\n",
" 'HHHHHHHGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHIIHHIHFHHF'),\n",
" ('509.6.64.20524.149722/2',\n",
" 'TAAGTCAGGATACTTTCCCATATCCCAGCCCTGCTCCNTCTTTAAATAAT',\n",
" 'HHHHHHHHHHHHHHHHHHHH@HHFHHHEFHHHHHHFF#FFFFFFFHHHHH')),\n",
" (('509.4.62.19231.2763/1',\n",
" 'GTTGATAAGCAAGCATCTCATTTTGTGCATATACCTGGTCTTTCGTATTC',\n",
" 'HHHHHHHHHHHHHHEHHHHHHHHHHHHHHHHHHHHHHHDHHHHHHGHGHH'),\n",
" ('509.4.62.19231.2763/2',\n",
" 'CTCTGCTGGTATGGTTGACGCCGGATTTGAGAATCAANAAGAGCTTACTA',\n",
" 'HHHHHHHHHHHHHHHHHHEHEHHHFHGHHHHHHHH>@#@=44465HHHHH'))]"
]
}
],
"prompt_number": 11
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Other comments\n",
"\n",
"In all the examples above, the reads in the FASTQ file are all the same length. This is not necessarily the case though it is usually true for datasets generated by sequencing-by-synthesis instruments. FASTQ files can contain reads of various lengths.\n",
"\n",
"FASTQ files often have extension `.fastq` or `.fq`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Other resources\n",
"\n",
"* [Wikipedia page for FASTQ format](http://en.wikipedia.org/wiki/Fastq_format)\n",
"* [BioPython], which has [its own ways of parsing FASTA](http://biopython.org/wiki/SeqIO)\n",
"* [FASTX] toolkit\n",
"* [seqtk]\n",
"* [FastQC]\n",
"\n",
"[BioPython]: http://biopython.org/wiki/Main_Page\n",
"[SeqIO]: http://biopython.org/wiki/SeqIO\n",
"[SAMtools]: http://samtools.sourceforge.net/\n",
"[FASTX]: http://hannonlab.cshl.edu/fastx_toolkit/\n",
"[FASTQC]: http://www.bioinformatics.babraham.ac.uk/projects/fastqc/\n",
"[seqtk]: https://github.com/lh3/seqtk\n",
"\n",
"\u00a9 Copyright Ben Langmead 2014"
]
}
],
"metadata": {}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment