超越for循环:对大型,格式良好的数据文件进行高性能分析

时间:2015-07-25 19:22:22

标签: python performance parsing bioinformatics

我希望使用python来优化大数据解析问题的性能。如果有人感兴趣:下面显示的数据是六种灵长类物种的全基因组DNA序列比对的片段。

目前,我知道如何处理这类问题的最好方法是打开每个~250(大小为20-50MB)的文件,逐行循环并提取我想要的数据。格式(在示例中显示)是相当规则的,尽管每个10-100万个线段都有重要的变化。循环工作正常,但速度很慢。

我最近一直在使用numpy来处理大量(> 10 GB)数值数据集,我对能够多快地对数组执行不同的计算印象深刻。我想知道是否有一些高性能的解决方案来处理格式化的文本,以避免繁琐的for循环?

我的文件包含多个具有模式

的细分
<MULTI-LINE HEADER>  # number of header lines mirrors number of data columns
<DATA BEGIN FLAG>  # the word 'DATA'
<DATA COLUMNS>  # variable number of columns
<DATA END FLAG>  # the pattern '//'
<EMPTY LINE>

示例:

# key to the header fields:
# header_flag chromosome segment_start segment_end quality_flag chromosome_data
SEQ homo_sapiens 1 11388669 11532963 1 (chr_length=249250621)
SEQ pan_troglodytes 1 11517444 11668750 1 (chr_length=229974691)
SEQ gorilla_gorilla 1 11607412 11751006 1 (chr_length=229966203)
SEQ pongo_pygmaeus 1 218866021 219020464 -1 (chr_length=229942017)
SEQ macaca_mulatta 1 14425463 14569832 1 (chr_length=228252215)
SEQ callithrix_jacchus 7 45949850 46115230 1 (chr_length=155834243)
DATA
GGGGGG
CCCCTC
......  # continue for 10-100 thousand lines
//

SEQ homo_sapiens 1 11345717 11361846 1 (chr_length=249250621)
SEQ pan_troglodytes 1 11474525 11490638 1 (chr_length=229974691)
SEQ gorilla_gorilla 1 11562256 11579393 1 (chr_length=229966203)
SEQ pongo_pygmaeus 1 219047970 219064053 -1 (chr_length=229942017)
DATA
CCCC
GGGG
....  # continue for 10-100 thousand lines
//

<ETC>

我将使用物种homo_sapiensmacaca_mulatta都存在于标题中的片段,而字段6(我在上面的注释中称为质量标志)等于每个物种的“1” 。由于macaca_mulatta未出现在第二个示例中,因此我会完全忽略此细分。

我只关心segment_start的{​​{1}}和segment_end坐标,因此在存在homo_sapiens的细分中,我会记录这些字段并将其用作关键字homo_sapiensdict()还告诉我segment_start的第一个位置坐标,它对当前细分中的每一行数据严格增加1。

我想比较homo_sapienshomo_sapiens的字母(DNA碱基)。 macaca_mulattahomo_sapiens出现的标题行(即第一个例子中的1和5)对应于表示它们各自序列的数据列。

重要的是,这些列并不总是相同的,所以我需要检查标题以获得每个段的正确索引,并检查两个物种是否在当前段中。 < / p>

查看示例1中的两行数据,我的相关信息是

macaca_mulatta

对于包含# homo_sapiens_coordinate homo_sapiens_base macaca_mulatta_base 11388669 G G 11388670 C T homo_sapiens信息的每个细分,我会从标题中记录macaca_mulatta的开始和结束,并将两个不匹配的每个位置记录到列表中。最后,一些职位有“缺口”或质量较低的数据,即

homo_sapiens

我只会记录aaa--A homo_sapiens都有有效基数的位置(必须在集合macaca_mulatta中),所以我考虑的最后一个变量是每个有效碱基的计数器分割。

我给定文件的最终数据结构是一个字典,如下所示:

ACGT

这是我使用for循环编写的函数:

{(segment_start=i, segment_end=j, valid_bases=N): list(mismatch positions), 
    (segment_start=k, segment_end=l, valid_bases=M): list(mismatch positions), ...}

这段代码工作得很好(也许它可以使用一些调整),但我真正的问题是,如果没有运行for循环并检查每一行,是否可能有更快的方法来提取这些数据?例如,使用def human_macaque_divergence(chromosome): """ A function for finding the positions of human-macaque divergent sites within segments of species alignment tracts :param chromosome: chromosome (integer: :return div_dict: a dictionary with tuple(segment_start, segment_end, valid_bases_in_segment) for keys and list(divergent_sites) for values """ ch = str(chromosome) div_dict = {} with gz.open('{al}Compara.6_primates_EPO.chr{c}_1.emf.gz'.format(al=pd.align, c=ch), 'rb') as f: # key to the header fields: # header_flag chromosome segment_start segment_end quality_flag chromosome_info # SEQ homo_sapiens 1 14163 24841 1 (chr_length=249250621) # flags, containers, counters and indices: species = [] starts = [] ends = [] mismatch = [] valid = 0 pos = -1 hom = None mac = None species_data = False # a flag signalling that the lines we are viewing are alignment columns for line in f: if 'SEQ' in line: # 'SEQ' signifies a segment info field assert species_data is False line = line.split() if line[2] == ch and line[5] == '1': # make sure that the alignment is to the desired chromosome in humans quality_flag is '1' species += [line[1]] # collect each species in the header starts += [int(line[3])] # collect starts and ends ends += [int(line[4])] if 'DATA' in line and {'homo_sapiens', 'macaca_mulatta'}.issubset(species): species_data = True # get the indices to scan in data columns: hom = species.index('homo_sapiens') mac = species.index('macaca_mulatta') pos = starts[hom] # first homo_sapiens positional coordinate continue if species_data and '//' not in line: assert pos > 0 # record the relevant bases: human = line[hom] macaque = line[mac] if {human, macaque}.issubset(bases): valid += 1 if human != macaque and {human, macaque}.issubset(bases): mismatch += [pos] pos += 1 elif species_data and '//' in line: # '//' signifies segment boundary # store segment results if a boundary has been reached and data has been collected for the last segment: div_dict[(starts[hom], ends[hom], valid)] = mismatch # reset flags, containers, counters and indices species = [] starts = [] ends = [] mismatch = [] valid = 0 pos = -1 hom = None mac = None species_data = False elif not species_data and '//' in line: # reset flags, containers, counters and indices species = [] starts = [] ends = [] pos = -1 hom = None mac = None return div_dict 加载整个文件只需不到一秒钟,但它会创建一个非常复杂的字符串。 (原则上,我假设我可以使用正则表达式来解析至少一些数据,例如标题信息,但我不确定这是否必然会增加性能,如果没有一些批处理方法来处理每个数据列中的每个数据列段)。

有没有人对如何绕过数十亿行循环并以更大批量的方式解析这种文本文件有任何建议?

如果评论中有任何不清楚的地方,请告诉我,很高兴编辑或直接回复以改善帖子!

5 个答案:

答案 0 :(得分:1)

是的,您可以使用一些正则表达式一次性提取数据;这可能是努力/表现的最佳比例。

如果您需要更多性能,可以使用mx.TextTools构建有限状态机;我非常有信心这会快得多,但是编写规则和学习曲线所需的努力可能会让你感到沮丧。

您还可以将数据拆分为块并并行处理,这可能有所帮助。

答案 1 :(得分:1)

如果您有工作代码并需要提高性能,请使用分析器并一次测量一个优化的效果。 (即使你不使用探查器,也一定要使用探测器。)你现在的代码看起来很合理,也就是说,在性能方面我没有看到任何“愚蠢”。

话虽如此,对所有字符串匹配使用预编译正则表达式可能是值得的。通过使用re.MULTILINE,您可以将整个文件作为字符串读取并拉出部分行。例如:

s = open('file.txt').read()
p = re.compile(r'^SEQ\s+(\w+)\s+(\d+)\s+(\d+)\s+(\d+)', re.MULTILINE)
p.findall(s)

产生

[('homo_sapiens', '1', '11388669', '11532963'),
 ('pan_troglodytes', '1', '11517444', '11668750'),
 ('gorilla_gorilla', '1', '11607412', '11751006'),
 ('pongo_pygmaeus', '1', '218866021', '219020464'),
 ('macaca_mulatta', '1', '14425463', '14569832'),
 ('callithrix_jacchus', '7', '45949850', '46115230')]

然后,您需要对此数据进行后处理以处理代码中的特定条件,但总体结果可能会更快。

答案 2 :(得分:1)

你可以将relist comprehensions中的一些花哨的压缩文件结合起来,它可以取代for循环,并尝试挤压一些性能提升。下面我概述了将读入的数据文件分割为整个字符串的策略:

import re
from itertools import izip #(if you are using py2x like me, otherwise just use zip for py3x)

s = open('test.txt').read()

现在找到所有标题行,以及大字符串中相应的索引范围

head_info = [(s[m.start():m.end()],m.start(), m.end()) for m in re.finditer('\nSEQ.*', s)]
head = [ h[0] for h in head_info]
head_inds = [ (h[1],h[2]) for h in head_info]

#head
#['\nSEQ homo_sapiens 1 11388669 11532963 1 (chr_length=249250621)',
# '\nSEQ pan_troglodytes 1 11517444 11668750 1 (chr_length=229974691)',
# '\nSEQ gorilla_gorilla 1 11607412 11751006 1 (chr_length=229966203)',
# '\nSEQ pongo_pygmaeus 1 218866021 219020464 -1 (chr_length=229942017)',
# '\nSEQ macaca_mulatta 1 14425463 14569832 1 (chr_length=228252215)',
# '\nSEQ callithrix_jacchus 7 45949850 46115230 1 (chr_length=155834243)',
# '\nSEQ homo_sapiens 1 11345717 11361846 1 (chr_length=249250621)',
#...
#head_inds
#[(107, 169),
# (169, 234),
# (234, 299),
# (299, 366),
# (366, 430),
# (430, 498),
# (1035, 1097),
# (1097, 1162)
# ...

现在,对数据(带基数的代码行)执行相同的操作

data_info = [(s[m.start():m.end()],m.start(), m.end()) for m in re.finditer('\n[AGCT-]+.*', s)]
data = [ d[0] for d in data_info]
data_inds = [ (d[1],d[2]) for d in data_info]

现在,只要有新细分,head_inds[i][1]head_inds[i+1][0]之间就会存在不连续性。 data_inds也是如此。我们可以使用这些知识来查找每个段的开头和结尾,如下所示

head_seg_pos = [ idx+1 for idx,(i,j) in enumerate( izip( head_inds[:-1], head_inds[1:]))  if j[0]-i[1]]
head_seg_pos = [0] + head_seg_pos + [len(head_seg_pos)] # add beginning and end which we will use next
head_segmented = [ head[s1:s2] for s1,s2 in izip( head_seg_pos[:-1], head_seg_pos[1:]) ]
#[['\nSEQ homo_sapiens 1 11388669 11532963 1 (chr_length=249250621)',
#  '\nSEQ pan_troglodytes 1 11517444 11668750 1 (chr_length=229974691)',
#  '\nSEQ gorilla_gorilla 1 11607412 11751006 1 (chr_length=229966203)',
#  '\nSEQ pongo_pygmaeus 1 218866021 219020464 -1 (chr_length=229942017)',
#  '\nSEQ macaca_mulatta 1 14425463 14569832 1 (chr_length=228252215)',
#  '\nSEQ callithrix_jacchus 7 45949850 46115230 1 (chr_length=155834243)'],
#['\nSEQ homo_sapiens 1 11345717 11361846 1 (chr_length=249250621)',
#  '\nSEQ pan_troglodytes 1 11474525 11490638 1 (chr_length=229974691)',
# ...

和数据相同

data_seg_pos = [ idx+1 for idx,(i,j) in enumerate( izip( data_inds[:-1], data_inds[1:]))  if j[0]-i[1]]
data_seg_pos = [0] + data_seg_pos + [len(data_inds)] # add beginning and end for the next step
data_segmented = [ data[s1:s2] for s1,s2 in izip( data_seg_pos[:-1], data_seg_pos[1:]) ]

现在我们可以对分段数据和分段标题进行分组,并且只保留包含homo_sapiens和macaca_mulatta数据的组

groups = [ [h,d] for h,d in izip( head_segmented, data_segmented) if all( [sp in ''.join(h) for sp in ('homo_sapiens','macaca_mulatta')] ) ]

现在你有一个groups数组,其中每个组都有

group[0][0] #headers for segment 0
#['\nSEQ homo_sapiens 1 11388669 11532963 1 (chr_length=249250621)',
# '\nSEQ pan_troglodytes 1 11517444 11668750 1 (chr_length=229974691)',
# '\nSEQ gorilla_gorilla 1 11607412 11751006 1 (chr_length=229966203)',
# '\nSEQ pongo_pygmaeus 1 218866021 219020464 -1 (chr_length=229942017)',
# '\nSEQ macaca_mulatta 1 14425463 14569832 1 (chr_length=228252215)',
# '\nSEQ callithrix_jacchus 7 45949850 46115230 1 (chr_length=155834243)']
groups[0][1] # data from segment 0
#['\nGGGGGG',
# '\nCCCCTC',
# '\nGGGGGG',
# '\nGGGGGG',
# '\nGGGGGG',
# '\nGGGGGG',
# '\nGGGGGG',
# '\nGGGGGG',
# '\nGGGGGG',
# ...

处理的下一步我将留给你,所以我不会偷走所有的乐趣。但希望这可以让您使用list comprehension来优化代码。

更新

考虑简单的测试用例来衡量理解效率和re:

def test1():
    with open('test.txt','r') as f:
        head = []
        for line in f:
            if line.startswith('SEQ'):
               head.append( line)
        return head

def test2():
    s = open('test.txt').read()
    head = re.findall( '\nSEQ.*', s)
    return head

%timeit( test1() )
10000 loops, best of 3: 78 µs per loop

%timeit( test2() )
10000 loops, best of 3: 37.1 µs per loop

即使我使用re

收集其他信息
def test3():
    s         = open('test.txt').read()
    head_info = [(s[m.start():m.end()],m.start(), m.end()) for m in re.finditer('\nSEQ.*', s)]

    head = [ h[0] for h in head_info]
    head_inds = [ (h[1],h[2]) for h in head_info]

%timeit( test3() )
10000 loops, best of 3: 50.6 µs per loop

我仍然可以获得速度提升。我相信在你的情况下使用列表推导可能会更快。然而,for循环可能实际上击败了理解(我收回我之前说的),考虑

def test1(): #similar to how you are reading in the data in your for loop above
    with open('test.txt','r') as f:
        head = []
        data = []
        species = []
        species_data = False
        for line in f:
            if line.startswith('SEQ'):
                head.append( line)
                species.append( line.split()[1] )
                continue
            if 'DATA' in line and {'homo_sapiens', 'macaca_mulatta'}.issubset(species):
                species_data = True
                continue
            if species_data and '//' not in line:
                data.append( line )
                continue
            if species_data and line.startswith( '//' ):
                species_data = False
                species = []
                continue
        return head, data

def test3():
    s         = open('test.txt').read()
    head_info = [(s[m.start():m.end()],m.start(), m.end()) for m in re.finditer('\nSEQ.*', s)]
    head = [ h[0] for h in head_info]
    head_inds = [ (h[1],h[2]) for h in head_info]

    data_info = [(s[m.start():m.end()],m.start(), m.end()) for m in re.finditer('\n[AGCT-]+.*', s)]
    data = [ h[0] for h in data_info]
    data_inds = [ (h[1],h[2]) for h in data_info]

    return head,data

在这种情况下,随着迭代变得更加复杂,传统的for循环获胜

In [24]: %timeit(test1()) 
10000 loops, best of 3: 135 µs per loop

In [25]: %timeit(test3())
1000 loops, best of 3: 256 µs per loop

虽然我仍然可以使用re.findall两次并击败for循环:

def test4():
    s         = open('test.txt').read()
    head = re.findall( '\nSEQ.*',s )
    data = re.findall( '\n[AGTC-]+.*',s)
    return head,data

In [37]: %timeit( test4() )
10000 loops, best of 3: 79.5 µs per loop

我想随着每次迭代的处理变得越来越复杂,for循环将会获胜,尽管可能有更聪明的方式继续使用re。我希望有一种标准方法可以确定何时使用它们。

答案 3 :(得分:1)

您的代码看起来不错,但有些特殊事情可以改进,例如使用map等。

有关Python性能提示的良好指南,请参阅:

https://wiki.python.org/moin/PythonSpeed/PerformanceTips

我使用上面的提示让代码的工作速度几乎和C代码一样快。基本上,尽量避免使用for循环(使用map),尝试使用find内置函数等。让Python尽可能地为工作内置函数,主要用C语言编写。

一旦获得可接受的性能,您可以使用以下方式并行运行:

https://docs.python.org/dev/library/multiprocessing.html#module-multiprocessing

编辑:

我也刚刚意识到你正在打开一个压缩的gzip文件。我怀疑解压缩它需要花费大量时间。您可以尝试通过以下方式多线程化来加快速度:

https://code.google.com/p/threadzip/

答案 4 :(得分:0)

使用Numpy进行文件处理

数据本身看似完全正常,可以使用Numpy轻松处理。标题只是文件的一小部分,其处理速度不是很相关。所以我们的想法是只为原始数据切换到Numpy,除此之外,保持现有的循环。

如果可以从标题中确定数据段中的行数,则此方法效果最佳。对于这个答案的其余部分,我认为确实如此。如果可能,则必须用例如数据段来确定数据段的起点和终点。 str.find或正则表达式。这仍将在&#34;编译的C速度&#34;但缺点是该文件必须循环两次。在我看来,如果你的文件只有50MB,那么将一个完整的文件加载到RAM中并不是一个大问题。

E.g。在if species_data and '//' not in line:

下添加以下内容
# Define `import numpy as np` at the top

# Determine number of rows from header data. This may need some 
# tuning, if possible at all
nrows = max(ends[i]-starts[i] for i in range(len(species)))

# Sniff line length, because number of whitespace characters uncertain
fp = f.tell()
ncols = len(f.readline())
f.seek(fp)

# Load the data without loops. The file.read method can do the same,
# but with numpy.fromfile we have it in an array from the start.
data = np.fromfile(f, dtype='S1', count=nrows*ncols)
data = data.reshape(nrows, ncols)

# Process the data without Python loops. Here we leverage Numpy
# to really speed up the processing.
human   = data[:,hom]
macaque = data[:,mac]

valid = np.in1d(human, bases) & np.in1d(macaque, bases)
mismatch = (human != macaque)
pos = starts[hom] + np.flatnonzero(valid & mismatch)

# Store
div_dict[(starts[hom], ends[hom], valid.sum())] = pos

# After using np.fromfile above, the file pointer _should_ be exactly
# in front of the segment termination flag
assert('//' in f.readline())

# Reset the header containers and flags 
...

因此elif species_data and '//' in line:情况变得多余,容器和标志可以在与上面相同的块中重置。或者,您也可以删除assert('//' in f.readline())并保留elif species_data and '//' in line:大小写并重置容器和标记。

注意事项

为了依赖文件指针在处理标题和数据之间切换,有一点需要注意:(在CPython中)迭代文件对象uses a read-ahead buffer,导致文件指针比你更低的文件指针# 39; d期待。然后,当您对该文件指针使用numpy.fromfile时,它会跳过段开头的数据,而且会读入下一段的标题。这可以通过专门使用file.readline方法来解决。我们可以方便use it as an iterator这样:

for line in iter(f.readline, ''):
    ...

要确定使用numpy.fromfile读取的字节数,还有另一个警告:有时在一行的末尾有一个line termination character \n,有时两个{{1} }}。第一个是Linux / OSX上的约定,后者是Windows上的约定。有\r\n来确定默认值,但很明显,对于文件解析,这不够强大。因此,在上面的代码中,数据行的长度是通过实际读取一行来确定的,检查os.linesep,然后将文件指针放回到行的开头。

当您遇到数据段(len)并且未包含所需物种时,您应该能够计算下一段标题的'DATA' in lineoffset 。比循环数据更好,你甚至不感兴趣!