Perl 统计数百GB数据中的子序列

Perl 统计数百GB数据中的子序列,perl,memory,substring,large-files,Perl,Memory,Substring,Large Files,我试图处理一个非常大的文件,并统计文件中特定长度的所有序列的频率 为了说明我在做什么,考虑一个小的输入文件,包含序列 ABCFDABCGBBDBCABEFEBFEBFEB < /COD> < /P> 下面,代码读取整个文件,并获取长度为n的第一个子字符串(下面我将其设置为5,尽管我希望能够更改它),并计算其频率: abcde => 1 下一行,它向右移动一个字符,并执行相同的操作: bcdef => 1 然后,它继续处理字符串的其余部分,并打印5个最频繁的序列: open my

我试图处理一个非常大的文件,并统计文件中特定长度的所有序列的频率

为了说明我在做什么,考虑一个小的输入文件,包含序列<代码> ABCFDABCGBBDBCABEFEBFEBFEB < /COD> < /P> 下面,代码读取整个文件,并获取长度为n的第一个子字符串(下面我将其设置为5,尽管我希望能够更改它),并计算其频率:

abcde => 1
下一行,它向右移动一个字符,并执行相同的操作:

bcdef => 1
然后,它继续处理字符串的其余部分,并打印5个最频繁的序列:

open my $in, '<', 'in.txt' or die $!; # 'abcdefabcgbacbdebdbbcaebfebfebfeb'

my $seq = <$in>; # read whole file into string
my $len = length($seq);

my $seq_length = 5; # set k-mer length
my %data;

for (my $i = 0; $i <= $len - $seq_length; $i++) {
     my $kmer = substr($seq, $i, $seq_length);
     $data{$kmer}++;
}

# print the hash, showing only the 5 most frequent k-mers
my $count = 0;
foreach my $kmer (sort { $data{$b} <=> $data{$a} } keys %data ){
    print "$kmer $data{$kmer}\n";
    $count++;
    last if $count >= 5;
}

然而,我想找到一种更有效的方法来实现这一目标。如果输入文件是10GB或1000GB,那么将整个文件读入字符串将非常耗费内存

我曾考虑过按字符块阅读,比如说一次读100个字符,然后按照上面的步骤进行,但是在这里,跨越两个字符块的序列不会被正确计算

我的想法是,只从字符串中读取n个字符,然后移动到下一个n个字符,并执行相同的操作,在哈希中计算它们的频率,如上所述

  • 对于我如何做到这一点,有什么建议吗?我用偏移量看了一眼a,但我不知道如何在这里合并它
  • substr
    是执行此任务最有效的内存工具吗
从您自己的代码来看,您的数据文件似乎只有一行数据——没有被换行符打断——因此我在下面的解决方案中假设了这一点。即使行的末尾可能有一个换行符,在末尾选择五个最频繁的子序列也会将其抛出,因为它只发生一次

该程序用于从文件中获取任意大小的数据块,并将其附加到内存中已有的数据中

循环的主体大部分与您自己的代码相似,但我使用了
for
的列表版本,而不是C风格的版本,因为它更清晰

在处理每个块之后,内存中的数据被截断为最后一个
SEQ_LENGTH-1
字节,然后下一个循环从文件中提取更多数据

我还使用常数表示K-mer大小和块大小。它们毕竟是不变的

输出数据是在
CHUNK_SIZE
设置为7的情况下生成的,因此会有许多跨边界子序列的实例。它匹配您自己所需的输出,但最后两个计数为1的条目除外。这是因为Perl哈希键固有的随机顺序,如果您需要具有相等计数的特定序列顺序,则必须指定它,以便我可以更改排序

use strict;
use warnings 'all';

use constant SEQ_LENGTH => 5;           # K-mer length
use constant CHUNK_SIZE => 1024 * 1024; # Chunk size - say 1MB

my $in_file = shift // 'in.txt';

open my $in_fh, '<', $in_file or die qq{Unable to open "$in_file" for input: $!};

my %data;
my $chunk;
my $length = 0;

while ( my $size = sysread $in_fh, $chunk, CHUNK_SIZE, $length ) {

    $length += $size;

    for my $offset ( 0 .. $length - SEQ_LENGTH ) {
         my $kmer = substr $chunk, $offset, SEQ_LENGTH;
         ++$data{$kmer};
    }

    $chunk = substr $chunk, -(SEQ_LENGTH-1);
    $length = length $chunk;
}

my @kmers = sort { $data{$b} <=> $data{$a} } keys %data;
print "$_ $data{$_}\n" for @kmers[0..4];
注意行:
$chunk=substr$chunk,-(SEQ_LENGTH-1)
当我们通过
while
循环时设置
$chunk
。这可以确保跨越2个块的字符串得到正确计数


$chunk=substr$chunk,-4
语句从当前块中删除除最后四个字符以外的所有字符,以便下次读取时将文件中的
chunk\u SIZE
字节追加到其余字符。这样,搜索将继续,但除了下一个块外,还将从上一个块的最后4个字符开始:数据不会落入块之间的“裂缝”中。

最简单的方法是使用函数:

% time perl -e '$/ = \1048576; 
           while ($s = <>) { for $i (0..length $s) { 
             $hash{ substr($s, $i, 5) }++ } }  
           foreach my $k (sort { $hash{$b} <=> $hash{$a} } keys %hash) {
             print "$k $hash{$k}\n"; $it++; last if $it == 5;}' nucleotide.data  
NNCTA 337530
GNGGA 337362
NCACT 337304
GANGN 337290
ACGGC 337210
      269.79 real       268.92 user         0.66 sys    
在一个测试数据文件
nucleotice.data
上,Borodin的脚本和上面显示的方法都产生了相同的前五名结果。请注意,与上面的shell命令的结果相比,差异很小。这说明了需要正确地处理分块读取的数据

NNCTA 337530
GNGGA 337362
NCACT 337305
GANGN 337290
ACGGC 337210
基于脚本的速度明显加快:

time perl sequence_search_stream-reader.pl nucleotide.data   
252.12s
time perl sequence_search_borodin.pl nucleotide.data     
350.57s
文件
nucleous.data
大小为1Gb,由约10亿个字符的单个字符串组成:

% wc nucleotide.data
       0       0 1048576000 nucleotide.data
% echo `head -c 20 nucleotide.data`
NCCANGCTNGGNCGNNANNA
我使用此命令创建文件:

perl -MString::Random=random_regex -e '
 open (my $fh, ">>", "nucleotide.data");
 for (0..999) { print $fh random_regex(q|[GCNTA]{1048576}|) ;}'

列表和字符串

由于应用程序应该一次读取一个块,并沿着数据长度移动这个
$seq_length
大小的窗口,构建一个用于跟踪字符串频率的哈希,因此我认为“惰性列表”方法可能在这里起作用。但是,要在数据集合(或与读取元素一样)中移动窗口,需要一个列表

我将数据视为一个非常长的字符串,首先必须将其放入列表中,才能使这种方法发挥作用。我不确定这能有多有效。尽管如此,我还是尝试用“懒散列表”的方法来解决这个问题:

use List::Gen 'slide';

$/ = \1048575; # Read a million character/bytes at a time.
my %hash;

while (my $seq = <>) {
  chomp $seq;
  foreach my $kmer (slide { join("", @_) } 5 => split //, $seq) {
    next unless length $kmer == 5;
    $hash{$kmer}++;
  }
}

foreach my $k (sort { $hash{$b} <=> $hash{$a} } keys %hash) {
  print "$k $hash{$k}\n";
  $it++; last if $it == 5;
}
8300万字符的核苷酸字符串

sequence_search_stream-reader.pl :     0.26s
sequence_search_borodin.pl       :     0.39s
sequence_search_listgen.pl       :     2.04s
使用文件
xaa
中的数据:

wc xaa
       0       1 83886080 xaa

% time perl sequence_search_stream-reader.pl xaa
GGCNG 31510
TAGNN 31182
AACTA 30944
GTCAN 30792
ANTAT 30756
       21.33 real        20.95 user         0.35 sys

% time perl sequence_search_borodin.pl xaa     
GGCNG 31510
TAGNN 31182
AACTA 30944
GTCAN 30792
ANTAT 30756
       28.13 real        28.08 user         0.03 sys

% time perl sequence_search_listgen.pl xaa 
GGCNG 31510
TAGNN 31182
AACTA 30944
GTCAN 30792
ANTAT 30756
      157.54 real       156.93 user         0.45 sys      
10亿字符的核苷酸字符串

sequence_search_stream-reader.pl :     0.26s
sequence_search_borodin.pl       :     0.39s
sequence_search_listgen.pl       :     2.04s
在一个较大的文件中,差异的大小相似,但由于编写时无法正确处理跨越块边界的序列,
List::Gen
脚本与本文开头的shell命令行具有相同的差异。较大的文件意味着大量块边界和计数差异

sequence_search_stream-reader.pl :   252.12s
sequence_search_borodin.pl       :   350.57s
sequence_search_listgen.pl       :  1928.34s
区块边界问题当然可以解决,但我想知道使用“惰性列表”方法引入的其他潜在错误或瓶颈。如果使用
slide
沿字符串“懒洋洋地”移动在CPU使用率方面有任何好处,那么在开始之前需要从字符串中列出一个列表似乎是没有意义的

我并不奇怪跨块边界读取数据只是一种实现练习(可能无法“神奇地”处理),但我想知道还有哪些其他CPAN模块或老旧的子例程样式的解决方案可能存在


1。在每兆字节文件读取结束时跳过四个字符,从而跳过四个5个字符的字符串组合
sequence_search_stream-reader.pl :   252.12s
sequence_search_borodin.pl       :   350.57s
sequence_search_listgen.pl       :  1928.34s
echo "scale=10; 100 *  (1024^4/1024^2 ) * 4 / 1024^4 " | bc
.0003814697
$ perl -wE 'my $str = "abcdefabcgbacbdebdbbcaebfebfebfeb"; for my $pos (0..4) { $str =~ s/^.// if $pos; say for $str =~ m/(.{5})/g }'|sort|uniq -c|sort -nr|head -n 5
  3 ebfeb
  2 febfe
  2 bfebf
  1 gbacb
  1 fabcg
$ perl -wE 'my $str = "abcdefabcgbacbdebdbbcaebfebfebfeb"; my %occur; for my $pos (0..4) { substr($str, 0, 1) = "" if $pos; $occur{$_}++ for $str =~ m/(.{5})/gs }; for my $k (sort { $occur{$b} <=> $occur{$a} } keys %occur) { say "$occur{$k} $k" }'
3 ebfeb
2 bfebf
2 febfe
1 caebf
1 cgbac
1 bdbbc
1 acbde
1 efabc
1 aebfe
1 ebdbb
1 fabcg
1 bacbd
1 bcdef
1 cbdeb
1 defab
1 debdb
1 gbacb
1 bdebd
1 cdefa
1 bbcae
1 bcgba
1 bcaeb
1 abcgb
1 abcde
1 dbbca
my $str = "abcdefabcgbacbdebdbbcaebfebfebfeb";
my %occur;
for my $pos (0..4) {
    substr($str, 0, 1) = "" if $pos;
    $occur{$_}++ for $str =~ m/(.{5})/gs;
}

for my $k (sort { $occur{$b} <=> $occur{$a} } keys %occur) {
    say "$occur{$k} $k";
}
perl -e'
   use strict;
   use warnings qw( all );

   use constant SEQ_LENGTH => 20;
   use constant CHUNK_SIZE => 1024 * 1024;

   my $buf = "";
   while (1) {
      my $size = sysread(\*STDIN, $buf, CHUNK_SIZE, length($buf));
      die($!) if !defined($size);
      last if !$size;

      for my $offset ( 0 .. length($buf) - SEQ_LENGTH ) {
         print(substr($buf, $offset, SEQ_LENGTH), "\n");
      }

      substr($buf, 0, -(SEQ_LENGTH-1), "");
   }
' <in.txt >sequences.txt
sort sequences.txt >sorted_sequences.txt
perl -e'
   use strict;
   use warnings qw( all );

   my $last = "";           
   my $count;
   while (<>) {
      chomp;
      if ($_ eq $last) {
         ++$count;
      } else {
         print("$count $last\n") if $count;
         $last = $_;
         $count = 1;
      }
   }
' sorted_sequences.txt >counted_sequences.txt
sort -rns counted_sequences.txt >sorted_counted_sequences.txt
perl -e'
   use strict;
   use warnings qw( all );

   my $last_count;
   while (<>) {
      my ($count, $seq) = split;
      last if $. > 5 && $count != $last_count;
      print("$seq $count\n");
      $last_count = $count;
   }
' sorted_counted_sequences.txt