我正在尝试将字符串(A,T,C,G)映射为64位整数,其中每个字母使用此映射表示为两位:
mapping = {'A': 0b00, 'C': 0b01, 'G': 0b10, 'T': 0b11}
“sequence”字符串不会超过28个字符,我打算在整数开始时对整数进行零填充,使其为64位。目前,我使用以下功能,但速度非常慢。然后我通过调用:
转换输出int(result, 2)
这目前有效,但我想让这个功能非常快。我不太了解C ++,所以我很难移植到那里。我现在正在尝试Cython,但我也不熟悉它。任何帮助使Python(或甚至C ++或Cython等效)更高效的帮助将不胜感激。
下面是我的代码,我之后再调用int()。
def seq_to_binary(seq):
values = [mapping[c] for c in seq]
BITWIDTH = 2
return "".join(map(lambda x: bin(x)[2:].zfill(BITWIDTH), values)).encode();
在典型的序列输入中会出现类似的情况:'TGTGAGAAGCACCATAAAAGGCGTTGTG'
答案 0 :(得分:3)
您正在解释由4个不同的数字组成的字符串'作为一个数字,所以 base 4表示法。如果你有一串实际数字,在0-3范围内,你可以让int()
产生一个非常快的整数。
def seq_to_int(seq, _m=str.maketrans('ACGT', '0123')):
return int(seq.translate(_m), 4)
上述函数使用str.translate()
用匹配的数字替换4个字符中的每一个(我使用静态str.maketrans()
function来创建转换表)。然后,生成的数字字符串将被解释为基数为4的整数。
请注意,这会产生一个整数对象,而不是零和一个字符的二进制字符串:
>>> seq_to_int('TGTGAGAAGCACCATAAAAGGCGTTGTG')
67026852874722286
>>> format(seq_to_int('TGTGAGAAGCACCATAAAAGGCGTTGTG'), '016x')
'00ee20914c029bee'
>>> format(seq_to_int('TGTGAGAAGCACCATAAAAGGCGTTGTG'), '064b')
'0000000011101110001000001001000101001100000000101001101111101110'
这里不需要填充;只要您的输入序列是32个字母或更少,结果整数将适合无符号的8字节整数表示。在上面的输出示例中,我使用format()
字符串将该整数值分别格式化为十六进制和二进制字符串,并将这些表示填充为正确的64位数字位数。
要测量这是否更快,我们可以随机生成100万个测试字符串(每个字符长28个字符):
>>> from random import choice
>>> testvalues = [''.join([choice('ATCG') for _ in range(28)]) for _ in range(10 ** 6)]
上述功能可以在我的Macbook Pro上使用2.9 GHz Intel Core i7在3/4秒内产生100万次转换,在Python 3.6.5上:
>>> from timeit import timeit
>>> timeit('seq_to_int(next(tviter))', 'from __main__ import testvalues, seq_to_int; tviter=iter(testvalues)')
0.7316284350017668
这样每次通话的时间为0.73微秒。
(之前,我提倡预先计算版本,但经过实验,我发现了基础4的想法。)
要将此与此处发布的其他方法进行比较,有些需要进行调整以生成整数,并将其包含在函数中:
def seq_to_int_alexhall_a(seq, mapping={'A': b'00', 'C': b'01', 'G': b'10', 'T': b'11'}):
return int(b''.join(map(mapping.__getitem__, seq)), 2)
def seq_to_int_alexhall_b(seq, mapping={'A': b'00', 'C': b'01', 'G': b'10', 'T': b'11'}):
return int(b''.join([mapping[c] for c in seq]), 2)
def seq_to_int_jonathan_may(seq, mapping={'A': 0b00, 'C': 0b01, 'G': 0b10, 'T': 0b11}):
result = 0
for char in seq:
result = result << 2
result = result | mapping[char]
return result
然后我们可以比较这些:
>>> testfunctions = {
... 'Alex Hall (A)': seq_to_int_alexhall_a,
... 'Alex Hall (B)': seq_to_int_alexhall_b,
... 'Jonathan May': seq_to_int_jonathan_may,
... # base_decode as defined in https://stackoverflow.com/a/50239330
... 'martineau': base_decode,
... 'Martijn Pieters': seq_to_int,
... }
>>> setup = """\
... from __main__ import testvalues, {} as testfunction
... tviter = iter(testvalues)
... """
>>> for name, f in testfunctions.items():
... res = timeit('testfunction(next(tviter))', setup.format(f.__name__))
... print(f'{name:>15}: {res:8.5f}')
...
Alex Hall (A): 2.17879
Alex Hall (B): 2.40771
Jonathan May: 3.30303
martineau: 16.60615
Martijn Pieters: 0.73452
我建议的基础4方法很容易赢得这种比较。
答案 1 :(得分:1)
我的笨拙直接尝试使用Cython,这是迄今为止最好的解决方案(@ MartijnPieters)的两倍:
%%cython
ctypedef unsigned long long ull
cdef ull to_int(unsigned char *data, int n):
cdef ull res=0
cdef int i
cdef unsigned char ch
for i in range(n):
res<<=2
ch=data[i]
if ch==67: #C
res+=1
if ch==71: #G
res+=2
if ch==84: #T
res+=3
return res
cpdef str_to_int_ead(str as_str):
s=as_str.encode('ascii')
return to_int(s, len(s))
与目前的@ MartijnPieters解决方案相比,它在我的机器上快了两倍:
>>> [str_to_int_ead(x) for x in testvalues] == [seq_to_int(x) for x in testvalues]
True
>>> tviter=iter(testvalues)
>>> %timeit -n1000000 -r1 seq_to_int(next(tviter))
795 ns ± 0 ns per loop (mean ± std. dev. of 1 run, 1000000 loops each)
>>> tviter=iter(testvalues)
>>> %timeit -n1000000 -r1 str_to_int_ead(next(tviter))
363 ns ± 0 ns per loop (mean ± std. dev. of 1 run, 1000000 loops each)
整个运行时间为0.795秒对0.363秒(因此可以与@MartijnPieters测量的时间进行比较)。
一个人可以问,如果转换unicode&lt; - &gt;,可以节省多少开销。 ascii不需要?
%%cython
....
cpdef bytes_to_int_ead(bytes as_bytes):
return to_int(as_bytes, len(as_bytes))
>>> testbytes=[bytes(x.encode('ascii')) for x in testvalues]
>>> tviter=iter(testbytes)
>>> %timeit -n1000000 -r1 bytes_to_int_ead(next(tviter))
327 ns ± 0 ns per loop (mean ± std. dev. of 1 run, 1000000 loops each)
只快10% - 这有点令人惊讶......
但是,我们不应该忘记我们还测量“nexting”迭代器的开销,而不是我们得到:
>>> v=testvalues[0]
>>> %timeit str_to_int_ead(v)
>>> 139 ns ± 0.628 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
>>> v=testbytes[0]
>>> %timeit bytes_to_int_ead(v)
97.2 ns ± 1.03 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
现在实际上有大约40%的加速!
另一个有趣的结论:使用迭代器测试时,还有大约250ns(或70%)的开销。没有这个开销,cython击败了@ MartijnPieters的140ns vs 550ns,即几乎是4倍。
已将cython的列表函数与(MartijnPieters答案的当前状态)进行比较:
def seq_to_int(seq, _m=str.maketrans('ACGT', '0123')):
return int(seq.translate(_m), 4)
测试数据:
from random import choice
testvalues = [''.join([choice('ATCG') for _ in range(28)]) for _ in range(10 ** 6)]
答案 2 :(得分:0)
seq = 'TGTGAGAAGCACCATAAAAGGCGTTGTG'
mapping = {'A': b'00', 'C': b'01', 'G': b'10', 'T': b'11'}
result = b''.join(map(mapping.__getitem__, seq)).zfill(64)
print(result)
以下是一些比较选项的时间码:
import timeit
setup = """
seq = 'TGTGAGAAGCACCATAAAAGGCGTTGTG'
mapping = {'A': b'00', 'C': b'01', 'G': b'10', 'T': b'11'}
"""
for stmt in [
"b''.join(map(mapping.__getitem__, seq)).zfill(64)",
"b''.join([mapping[c] for c in seq]).zfill(64)",
]:
print(stmt)
print(timeit.timeit(stmt, setup, number=10000000))
我发现这两个选项大致相同,但结果可能会有所不同。
答案 3 :(得分:0)
使用位移运算符和加法。你有一个正确的想法,使用字典来保存字符代码:
mapping = {'A': 0b00, 'C': 0b01, 'G': 0b10, 'T': 0b11}
为此示例生成一个28个字符的字符串(这种字符串称为冗余,字符串可以):
chars = 'TGTGAGAAGCACCATAAAAGGCGTTGTG'
定义结果并将其设置为零:
result = 0
Python中的字符串实际上只是一个字符数组,您可以像遍历任何数组一样迭代字符串。我们将使用它以及嵌套的一系列位操作来执行您所需的操作:
for char in chars:
result = result << 2
result = result | mapping[char]
这将生成长度为2*len(chars)
的位,在本例中为56.要获得额外的
要添加额外的8位前导零,其整数表示实际上是QWORD(64位),并将自动用零填充8个最高位。
print(result)
>> 67026852874722286
如果您想要真正感兴趣,可以使用ctypes
加速代码。
答案 4 :(得分:0)
考虑这个问题的一种方法是要意识到它正在做的事情的本质是从基数4到基数10的转换。这可以通过多种方式完成,但我喜欢的是实际非常对Base 62 conversion问题的通用接受答案。
以下是默认情况下进行基本4转换的修改版本:
def base_decode(astring, alphabet="ACGT"):
"""Decode a Base X encoded astring into the number
Arguments:
- `astring`: The encoded astring
- `alphabet`: The alphabet to use for encoding
"""
base = len(alphabet)
strlen = len(astring)
num = 0
for idx, char in enumerate(astring):
power = (strlen - (idx + 1))
num += alphabet.index(char) * (base ** power)
return num
seq = 'TGTGAGAAGCACCATAAAAGGCGTTGTG'
print('seq_to_binary:', seq_to_binary(seq))
print('base_decode:', format(base_decode(seq), 'b'))
请注意,这实际上返回一个所需位长度的整数(整数在Python中是可变长度),以存储作为填充为二进制整数值的字符串给出的数字。对format()
的添加调用将该值转换为二进制字符串,因此可以将其打印并与调用seq_to_binary()
函数的结果进行比较,该函数返回字符串,不 a 64标题中提到的位整数。