Home > Community > Fast way to count number of reads and number of bases in a fastq file?
Upvote

27

Downvote
+ Bioinformatics
+ Biochemistry
+ Chemistry
Posted by
Louis Nardozi

Fast way to count number of reads and number of bases in a fastq file?

Anussukee Wijegunarathna  Follow

if the data is in SRA, there is sra-stat utility that returns reads,bases and quality distribution. these are stored in the SRA file. use --quick to get the stored stats or --statistics to calculate additional values broken down per readgroup/barcode.sra-stat --quick SRR077487

More

Upvote

VOTE

Downvote
Cathy Cintolo  Follow

The following is more than twice as fast; however, wc counts newline characters as well. We thus need to subtract the line count from the base count (using Bash):

fix_base_count() {    local counts=($(cat))    echo "${counts[0]} $((counts[1] - counts[0]))"}gunzip -c "$file" \| awk 'NR % 4 == 2' \| wc -cl \| fix_base_count

All the caveats from Simon’s comment apply: this assumes the “simple” FASTQ format, where each record consists of exactly four lines. I think this is true for all files produced by Illumina sequencers and downstream tools.

More

Upvote

VOTE

Downvote
Hannah Fulcher  Follow
command for benchmarking, and working on a 2.2GiB FASTQ file. The times on my machine were 9m5.628s and 3m5.825s real time, respectively. The user time in both cases was slightly elevated (11m vs 5m) to make the difference a factor 2.More
Upvote

VOTE

Downvote
Jeff Nelson  Follow
@terdon But given Simon’s answer and common sense, this is a fishy result indeed. I don’t know what my More
Upvote

VOTE

Downvote
EGA STaTuS  Follow
version is doing here.More
Upvote

VOTE

Downvote
Itamar Kasztelanski  Follow
@terdon I’ve used the More
Upvote

VOTE

Downvote
Jeremy Kennedy  Follow
What did you test this on? I used a 2.6G exome fastq file and this approach (without the bash function, so even faster) took 1m6.754s while my version using awk to count everything took 1m14.228s. What did you measure to see a two-fold increase in speed?More
Upvote

VOTE

Downvote
Andrew T  Follow

I get fairly quick results with my fastx-length.pl script, with the added bonus of being able to handle multi-line FASTQ files and displaying additional read-length QC statistics:

time zcat albacored_all.fastq.gz | /bioinf/scripts/fastx-length.pl > /dev/nullTotal sequences: 301135Total length: 283.902419 MbLongest sequence: 5.601 kbShortest sequence: 6 bMean Length: 942 bMedian Length: 999 bN50: 111835 sequences; L50: 1.103 kbN90: 245243 sequences; L90: 608 breal    0m8,802suser    0m16,584ssys 0m0,260s

Versus the script you have provided:

zcat albacored_all.fastq.gz | awk 'NR%4==2{c++; l+=length($0)}          END{                print "Number of reads: "c;                 print "Number of bases in reads: "l              }'Number of reads: 301135Number of bases in reads: 283902419real    0m8,382suser    0m10,216ssys 0m0,332s

Cat to /dev/null for comparison:

time zcat albacored_all.fastq.gz > /dev/nullreal    0m7,877suser    0m7,856ssys 0m0,020s

I suspect that something using bioawk might be a bit faster (and similarly FASTQ-compliant).

More

Upvote

VOTE

Downvote
John Hugo  Follow
bioinformatics.stackexchange.com/a/961/292More
Upvote

VOTE

Downvote
Hadiqa Ali  Follow
bioawk is not that efficient, at least in my tests: More
Upvote

VOTE

Downvote
Donald Hubbs  Follow

I hava implemented seqtk_counts using kseq.h from klib

Just a few line of Codes:

#include #include #include "kseq.h"KSEQ_INIT(gzFile, gzread)int main(int argc, char *argv[]){gzFile fp;kseq_t *seq;int l = 0;int64_t total = 0;int64_t lines = 0;if (argc == 1) {    fprintf(stderr, "Usage: %s  \n", argv[0]);    return 1;}fp = strcmp(argv[1], "-")? gzopen(argv[1], "r") : gzdopen(fileno(stdin), "r");seq = kseq_init(fp);while ((l = kseq_read(seq)) >= 0){    total += seq->seq.l;    lines += 1;}printf("%s\t%lld\t%lld\n", argv[2] ,(long long)lines, (long long)total);kseq_destroy(seq);gzclose(fp);return 0;

}

Compile it:

gcc  -O2  seqtk_counts.c  -o  seqtk_counts  -Iklib  -lz

Usage:

seqtk_counts foo.fasq.gz foo orcat foo.fasq.gz | seqtk_counts  - foo

More

Upvote

VOTE

Downvote
Gregg Schoenberger  Follow
Thanks for the edit! :) But isn't this basically the same program as shown in More
Upvote

VOTE

Downvote
James Stanford  Follow
sjcockell's answerMore
Upvote

VOTE

Downvote
Dave Jain  Follow

It's difficult to get this to go massively quicker I think - as with this question working with large gzipped FASTQ files is mostly IO-bound. We could instead focus on making sure we are getting the right answer.

People deride them too often, but this is where a well-written parser is worth it's weight in gold. Heng Li gives us this FASTQ Parser in C.

I downloaded the example tarball and modified the example code (excuse my C...):

#include #include #include "kseq.h"KSEQ_INIT(gzFile, gzread)int main(int argc, char *argv[]){    gzFile fp;    kseq_t *seq;    int l;    if (argc == 1) {        fprintf(stderr, "Usage: %s \n", argv[0]);        return 1;    }    fp = gzopen(argv[1], "r");    seq = kseq_init(fp);    int seqcount = 0;    long seqlen = 0;    while ((l = kseq_read(seq)) >= 0) {        seqcount = seqcount + 1;        seqlen = seqlen + (long)strlen(seq->seq.s);    }    kseq_destroy(seq);    gzclose(fp);    printf("Number of sequences: %d\n", seqcount);    printf("Number of bases in sequences: %ld\n", seqlen);    return 0;}

Then make and kseq_test foo.fastq.gz.

For my example file (~35m reads of ~75bp) this took:

real    0m49.670suser    0m49.364ssys     0m0.304s

Compared with your example:

real    0m43.616suser    1m35.060ssys     0m5.240s

Konrad's solution (in my hands):

real    0m39.682suser    1m11.900ssys     0m5.112s

(By the way, just zcat-ing the data file to /dev/null):

real    0m38.736suser    0m38.356ssys     0m0.308s

So, I get pretty close in speed, but am likely to be more standards compliant. Also this solution gives you more flexibility with what you can do with the data.

And my horrible C can almost certainly be optimised.


Same test, with kseq.h from Github, as suggested in the comments:

My machine is under different load this morning, so I've retested. Wall clock times:

OP: 0m44.813s

Konrad: 0m40.061s

zcat > /dev/null: 0m34.508s

kseq.h (Github): 0m32.909s

So most recent version of kseq.h is faster than simply zcat-ing the file (consistently in my tests...).

More

Upvote

VOTE

Downvote
Jane Thomson  Follow
Decompression will always be the bottleneck, it's the same situation we saw with More
Upvote

VOTE

Downvote
Jim Casey  Follow
from githubMore
Upvote

VOTE

Downvote
Eric Jones  Follow
. That's surprising. I guess there must be some fancy parsing going on inside More
Upvote

VOTE

Downvote
Jim Short  Follow
kseq_readMore
Upvote

VOTE

Downvote
Josh Velson  Follow
The kseq.h from the tarball is outdated. The one More
Upvote

VOTE

Downvote
more replies
Bobby Tatro  Follow

Use assembly-stats!

$ assembly-stats barcode13_filtered.fastq stats for barcode13_filtered.fastqsum = 2080834976, n = 656192, ave = 3171.08, largest = 15321N50 = 3598, n = 225863N60 = 3263, n = 286569N70 = 2920, n = 353904N80 = 2548, n = 429982N90 = 2081, n = 519671N100 = 500, n = 656192N_count = 0Gaps = 0real    0m7.418suser    0m5.976ssys     0m1.275s

Works on any fasta/fastq files, even those that aren't assemblies. And it's pretty fast!

Download from here: https://anaconda.org/bioconda/assembly-stats

More

Upvote

VOTE

Downvote
Jeff Liu  Follow
Thanks, that looks interesting. How does the time compare to the existing answers here? Is this faster? And what are the numbers reported? Is that 656192 reads? And 2080834976 bases?More
Upvote

VOTE

Downvote
Glenn Janot  Follow
This should too good to be true, is it because the More
Upvote

VOTE

Downvote
Ivan Bangov  Follow
Yes, yes, and yes! 656192 reads, 2080834976 bases, with the respective N50 etc values. And compared to the ones above, it is lightning fast. real 0m7.418s user 0m5.976sMore
Upvote

VOTE

Downvote
Jake Missy  Follow
Could you show an example of that? You are running this on a single, decompressed file here, while the question is asking about compressed files and the other answers have shown that the rate limiting step is usually the decompression. So how fast is this approach when run on a compressed file and taking the decompression time into account? Also, in order for the reported times to be meaningful, please also include the time taken by one of the other methods (you can't compare times across different machines and files).More
Upvote

VOTE

Downvote
Joseph Franek  Follow
is uncompressed? @sjcockell benchmark is on a comparable ~2.6e9 bases file, but it's about 5x slower... But it's a compressed file that was used.More
Upvote

VOTE

Downvote
Dave Howe  Follow

pigz | awk | wc is the fastest method

First off for benchmarks with FASTQ it's best to use a specific real-world example with a known answer. I've chosen this file:

ftp://ftp.1000genomes.ebi.ac.uk/vol1/ftp/phase3/data/HG01815/sequence_read/ERR047740_1.filt.fastq.gz

as my test file, the correct answers being:

Number of reads: 67051220Number of bases in reads: 6034609800

Next we want to find the fastest way possible to count these, all timings are the average wall-clock time (real) of 10 runs collected with the bash time on an otherwise unloaded system:

zgrep

zgrep . ERR047740_1.filt.fastq.gz |     awk 'NR%4==2{c++; l+=length($0)}          END{                print "Number of reads: "c;                 print "Number of bases in reads: "l              }'

This is the slowest method with an average run-time of 125.35 seconds

gzip awk

Using gzip we gain about another 10 seconds:

gzip -dc ERR047740_1.filt.fastq.gz |     awk 'NR%4==2{c++; l+=length($0)}          END{                print "Number of reads: "c;                 print "Number of bases in reads: "l              }'

Average run-time is 116.69 seconds

Konrad's gzip awk wc variant

fix_base_count() {    local counts=($(cat))    echo "${counts[0]} $((${counts[1]} - ${counts[0]}))"}gzip -dc ERR047740_1.filt.fastq.gz \    | awk 'NR % 4 == 2' \    | wc -cl \    | fix_base_count

This runs slower on this test file than the gzip awk variant of the solution, average run-time is 122.28 seconds.

kseq_test using latest kseq.h from klib

Code compiled with: gcc -O2 -o kseq_test kseq_test.c -lz where kseq_test.c is Simon's adaptation of Heng Li's FASTQ parser.

kseq_test ERR047740_1.filt.fastq.gz

Average run-time is 99.14 seconds, which is better than the gzip core utilities based solution so far, but we can do better!

piz awk

Using Mark Adler's pigz as a drop-in replacement for gzip, note that pigz gives us a speed gain as on top of gzip as in addition to the main deflate thread it uses another 3 threads for reading, writing and checksum calculations, see the man page for details.

pigz -dc ERR047740_1.filt.fastq.gz |     awk 'NR%4==2{c++; l+=length($0)}          END{                print "Number of reads: "c;                 print "Number of bases in reads: "l              }'

Average run-time is now 93.86 seconds, this is ~5 seconds faster than the kseq based C code but we can further improve the benchmark.

pigz awk wc

Next we use pigz as a drop in replacment for Konrad's wc variant of the awk based solution.

fix_base_count() {    local counts=($(cat))    echo "${counts[0]} $((${counts[1]} - ${counts[0]}))"}gzip -dc ERR047740_1.filt.fastq.gz \    | awk 'NR % 4 == 2' \    | wc -cl \    | fix_base_count

Average run-time is now down to 83.03 seconds, this is ~16 seconds faster than the kseq based solution and ~42 seconds faster than the OPs zgrep based solution.

Next as a baseline lets see just how much of this run-time is due to decompression of the input fastq.gz file.

gzip alone

gzip -dc ERR047740_1.filt.fastq.gz > /dev/null

Average run-time: 105.95 seconds, so the gzip based solutions (which also includes zcat and zgrep as these are provided by gzip) are never going to be faster than kseq_test.

pigz alone

pigz -dc ERR047740_1.filt.fastq.gz > /dev/null

Average run-time: 77.66 seconds, so quite clearly the additional three threads for read, write and checksum calculation offer a useful advantage. What's more this speed-up is greater when leveraging the awk | wc based solution, it's not clear why, but I expect this is due to the extra write thread.

Interestingly average CPU usage across all threads is quite revealing for the various answers, I've collated these stats using GNU time /usr/bin/time --verbose

zgrep based solution 133% - must be more than one thread somehow

gzip | awk based solution 99% - all gzip based solutions run single-threaded at 99% CPU usage

pigz | awk 147%

gzip | awk | wc 99% as with gzip

pgiz | awk | wc 155%

kseq_test 99%

gzip > dev/null 99%

pigz > dev/null 155%

Whilst the main deflate thread in pigz will run at 100% CPU load the extra 3 don't quite fully occupy additional cores to 100% (as is evidenced by average CPU usage of ~150%) they do however clearly result in reduced run-time.

I'm using Ubuntu 16.04.2 LTS**, my gzip, zcat, zgrep versions are all gzip 1.6 and pigz is version 2.3.1. gcc is version 5.4.0

** I think my patch level is actually 16.04.4 but I've not rebooted for 170 days :p

More

Upvote

VOTE

Downvote
Jerome Zoeller  Follow
wow excellent. thanksMore
Upvote

VOTE

Downvote
Ilan Elron  Follow
OK, confirmed. On your test file and on a local drive, More
Upvote

VOTE

Downvote
Eve Reckenbeil  Follow
zcat | grepMore
Upvote

VOTE

Downvote
Jon Kapecki  Follow
Nice and thorough! Thank you. I don't know why your tests showed pigz to be faster when it was slower in mine. I suspect it might be because I was testing using files stored on an NFS volume so my times could well have depended more on the network latency than anything else. I'll repeat your tests using a local drive and report back.More
Upvote

VOTE

Downvote
Jay Fraser  Follow
was indeed faster than More
Upvote

VOTE

Downvote
Andrew Yap  Follow

Using pyGATB

(I use the same file as in https://bioinformatics.stackexchange.com/a/400/292, same workstation as in https://bioinformatics.stackexchange.com/a/380/292)

$ time python3 -c "from gatb import Bank; seq_lens = [len(seq) for seq in Bank('SRR077487_2.filt.fastq.gz')]; print('Number of reads: %d' % len(seq_lens), 'Number of bases in reads: %d' % sum(seq_lens), sep='\n')"Number of reads: 23861612Number of bases in reads: 2386161200real    0m41.122suser    0m40.788ssys     0m0.312s

It is quite faster than bioawk:

$ time bioawk -c fastx '{nb_seq+=1; nb_char+=length($seq)} END {print "Number of reads: "nb_seq"\nNumber of bases in reads: "nb_char}' SRR077487_2.filt.fastq.gzNumber of reads: 23861612Number of bases in reads: 2386161200real    1m3.182suser    1m2.916ssys     0m0.268s

But not so much than the OP example:

$ time zgrep . SRR077487_2.filt.fastq.gz | awk 'NR%4==2{c++; l+=length($0)} END{print "Number of reads: "c; print "Number of bases in reads: "l}'Number of reads: 23861612Number of bases in reads: 2386161200real    0m47.127suser    1m36.292ssys     0m6.796s

Or than the wc based solution:

$ fix_base_count() {>     local counts=($(cat))>     echo "${counts[0]} $((${counts[1]} - ${counts[0]}))"> }$ time gunzip -c SRR077487_2.filt.fastq.gz | awk 'NR % 4 == 2' | wc -cl | fix_base_count23861612 2386161200real    0m44.915suser    1m12.000ssys     0m6.972s

I didn't compare with C-based solutions.

The zcat to /dev/null reference is the following:

$ time zcat SRR077487_2.filt.fastq.gz > /dev/nullreal    0m39.745suser    0m39.464ssys     0m0.252s

I'm still impressed by pyGATB speed

More

Upvote

VOTE

Downvote