如何加速itertools组合?

时间:2015-07-15 14:50:43

标签: python numpy combinations cython levenshtein-distance

作为我自己对基因型网络的研究的一部分,我有一段代码,我试图从字符串列表构建一个单差异网络。程序如下:

  1. 迭代所有字符串的成对组合。
  2. 如果字符串在一个位置上不同,则在网络中绘制它们之间的边缘。
  3. 如果他们没有一个位置不同,那么通过。
  4. 我现在拥有的代码块是这样的:

    from itertools import combinations
    import Levenshtein as lev # a package that wraps a C-implemented levenshtein distance
    import networkx as nx
    strings = [...list of strings...]
    
    G = nx.Graph()
    
    for (s1, s2) in combinations(strings, 2):
        if s1 not in G.nodes():
            G.add_node(s1)
        if s2 not in G.nodes():
            G.add_node(s2)
    
        if lev.distance(s1, s2) == 1:
            G.add_edge(s1, s2)
    

    显然没有办法提高图构建过程的计算复杂度 - 它总是O(n**2)。至少,在我有限的知识中,这就是我的想法 - 也许我错了?

    那说,考虑到我需要进行的比较次数的正常比例(〜约2000-5000),如果我可以获得几个数量级的加速,那么实际的计算时间将是仍然可以接受 - 使用当前的Python实现,构建图形需要大约几天。使用正确的导入(下面未说明),我在下面尝试了一个Cython实现,但无法弄清楚如何使其更快:

    cpdef cython_genotype_network(sequences):
    
        G = nx.Graph()
        cdef:
            unicode s1
            unicode s2
    
        for (s1, s2) in combinations(sequences, 2):
            if lev.distance(s1, s2) == 1:
                G.add_edge(s1, s2)
    
        return G
    

    特别是,Cython期望bytesstr s1而不是s2。该代码块会引发错误。

    所以......我来了两个问题:

    • Q1:Cython实施会有帮助吗?我如何修复字节与str错误?
    • Q2:是否可以使用numpy来解决此问题?从numpy矩阵转换为NetworkX很容易;但是,我似乎无法弄清楚如何在n-by-n空矩阵中广播Levenshtein距离函数,其中每行和每列对应一个字符串。

    更新1:如何生成样本数据

    生成字符串:

    from random import choice
    
    def create_random_nucleotides_python(num_nucleotides):
        """
        Creates random nucleotides of length num_nucleotides.
        """
    
        sequence = ''
    
        letters = ['A', 'T', 'G', 'C']
    
        for i in range(num_nucleotides):
            sequence = sequence + choice(letters)
    
        return sequence
    
    
    def mutate_random_position(string):
        """
        Mutates one position in the nucleotide sequence at random.
        """
    
        positions = [i for i in range(len(string))]
        pos_to_mut = choice(positions)
    
        letters = ['A', 'T', 'G', 'C']
    
        new_string = ''
        for i, letter in enumerate(string):
            if i == pos_to_mut:
                new_string = new_string + choice(letters)
            else:
                new_string = new_string + letter
    
        return new_string
    
    
    # Create 100 Python srings by mutating a first sequence, then randomly choosing stuff to mutate a single position.
    base_sequence = create_random_nucleotides_python(1000)
    
    sequences = [base_sequence]
    
    for i in range(99):
        sequence = choice(sequences)
        mutseq = mutate_random_position(sequence)
        sequences.append(mutseq)
    

2 个答案:

答案 0 :(得分:2)

关于复杂性:

您正在考虑每对琴弦。你不需要那个。您可以考虑每个字符串的所有1个距离字符串:

# I would start by populating the whole graph:
for s1 in strings:
    if s1 not in G.nodes():
        G.add_node(s1)
# Then compute the leven-1:
for s1 in strings:
    for s2 in variations(s1):
        if s2 in G.nodes():
            G.add_edge(s1, s2)

现在您的所有需求都是variations(string)短于O(n)

返回距离为1的所有变体(仅1次编辑|删除|插入)

def variations(string):
    for i in range(len(string)):
        # delete
        yield string[:i] + string[i+1:]
        # edit
        for l in 'ATGC':
            yield string[:i] + l + string[i+1:]
        # insert
        for l in 'ATGC':
            yield string[:i] + l + string[i:]

    # insert at the end
    for l in 'ATGC':
        yield string + l

现在,它的复杂性是O(m^2)(因为字符串连接),其中m是最长序列的大小。如果它已知,它是一个常数,现在只有O(1)

如果序列大小相同,则只能计算编辑。

否则,您可以将序列从最大到最小排序,并且只计算编辑和删除。

或者,您可以按大小对字符串进行排序,而不是将所有字符串与所有其他字符串进行比较,比较那些大小差异为< = 1的字符串。

答案 1 :(得分:0)

关于cython代码如何更快的一些想法

for (s1, s2) in combinations(sequences, 2):
    if lev.distance(s1, s2) == 1:
        G.add_edge(s1, s2)

我认为itertools.combinations是编译的,因此本身就是快速的。并且你调用它一次,所以for ...部分可能跟它一样快。尽管如此,在sequences中循环cython并不难。

lev.distance似乎使用已编译的代码。是否可以直接导入和调用该代码?查看lev来源。

我认为lev.distance最终会使用签名调用c函数:

distance_py(PyObject *self, PyObject *args)

或更基本

lev_edit_distance(size_t len1, const lev_byte *string1,
              size_t len2, const lev_byte *string2,
              int xcost)

https://github.com/ztane/python-Levenshtein/blob/master/Levenshtein/_levenshtein.c

G.add_edge做什么?是否有更简单的数据结构,您可以收集这些“边缘”。也许作为元组列表?

networkx是否提供了批量添加节点和边缘的方法?您是否总是必须使用add_nodeadd_edge?或者你可以给它一个列表节点,节点对(元组)列表?

看起来graph是Python词典或词典词典。

networkx允许您通过元组列表添加一堆边:

>>> G.add_edges_from([(1,2),(1,3)])

http://networkx.github.io/documentation/latest/tutorial/tutorial.html#edges

在python代码中,这篇文章可能是不必要的:

if s1 not in G.nodes():
    G.add_node(s1)
if s2 not in G.nodes():
    G.add_node(s2)

只是做:

G.add_nodes_from(strings)

http://networkx.github.io/documentation/latest/tutorial/tutorial.html#nodes