pandas Series:逐个比较所有值

3
我有一个包含两列的DataFrame,分别是"sequence"和"id",例如:
import pandas as pd
data = {"id":["seq1", "seq2", "seq3"], "sequence":["ATCTGC", "AACTGC", "AACTCC"]}
df = pd.DataFrame(data)

这是一个实际数据集的小例子,包含20,000多个序列。
我正在尝试以高效的方式获得一个序列距离矩阵。距离被理解为每个序列之间不同字符的数量。
为此,我需要在“序列”列中应用所有与所有值的函数。
我的当前代码如下:
def count_differences( seq, df ):
  return df.apply(lambda x: sum(1 for i, j in zip(x["sequence"], seq) if i != j), axis=1)

df2 = df.apply(lambda x: count_differences( x["sequence"], df), axis=1)
df2 = df2.rename(df["id"], axis="columns").rename(df["id"], axis="rows")

#      seq1 seq2 seq3
# seq1    0    1    2
# seq2    1    0    1
# seq3    2    1    0

这段文本的意思是:这是一个在应用程序中运行的应用程序。它可以正常工作,但是当我对所有序列运行它时,需要花费相当长的时间。有没有更有效的方法?我一直在尝试看看是否可以使用Series.map加速,但到目前为止我还没有找到任何解决方案。
1个回答

1
这是一个想法,涉及到使用numpy。有几个步骤,但由于底层工作是通过numpy数值数组完成的,因此可能更有效率。
import pandas as pd, numpy as np

data = {"id":["seq1", "seq2", "seq3"], "sequence":["ATCTGC", "AACTGC", "AACTCC"]}
df = pd.DataFrame(data)

a = np.array(list(map(list, df['sequence'])))
values = np.unique(a, return_inverse=True)[1].reshape(a.shape)

n = len(a)
d = {(i, j): np.sum(a[i]!=a[j]) for i in range(n) for j in range(n) if j > i}

res = np.zeros((n, n))
keys = list(zip(*d.keys()))

res[keys[0], keys[1]] = list(d.values())
res += res.T

df_res = pd.DataFrame(res, columns=data['id'], index=data['id'], dtype=int)

#       seq1  seq2  seq3
# seq1     0     1     2
# seq2     1     0     1
# seq3     2     1     0

解释

  • 将您的序列转换为numpy数组,其中每个元素都是一个字母。
  • 使用np.unique对数组进行因式分解(即将每个字母与一个数字相关联)。
  • 使用np.sum查找因式分解后的数组中行之间逐字差异,并将结果添加到字典中。由于结果是三角形的,因此只需执行一半的计算。
  • 从字典创建新的numpy数组,并添加反向操作以使三角形数组完整。
  • 将您的字典转换为数据帧。

性能基准测试

我看到了大约7倍的性能提升。

%timeit original(df)  # 3.32s
%timeit jp(df)        # 461ms

import pandas as pd, numpy as np

data = {"id":["seq1", "seq2", "seq3"], "sequence":["ATCTGC", "AACTGC", "AACTCC"]}
df = pd.DataFrame(data)

df = pd.concat([df]*100)

def original(df):
    def count_differences( seq, df ):
        return df.apply(lambda x: sum(1 for i, j in zip(x["sequence"], seq) if i != j), axis=1)

    df2 = df.apply(lambda x: count_differences( x["sequence"], df), axis=1)

    return df2

def jp(df):

    a = np.array(list(map(list, df['sequence'])))
    values = np.unique(a, return_inverse=True)[1].reshape(a.shape)

    n = len(a)
    d = {(i, j): np.sum(a[i]!=a[j]) for i in range(n) for j in range(n) if j > i}

    res = np.zeros((n, n))
    keys = list(zip(*d.keys()))

    res[keys[0], keys[1]] = list(d.values())
    res += res.T

    df_res = pd.DataFrame(res, columns=range(len(df['id'])), index=range(len(df['id'])), dtype=int)

    return df_res

我也尝试了一下我的更大的数据集,也得到了很好的改进,并检查输出的“DataFrames”是否相同。这是一个非常好的解决方案。不过我必须说,“values”被分配了但从未使用。我在我的代码中进行了注释,它仍然可以正常工作。我猜字符串比较也能很好地工作?我喜欢你如何将计算限制为一半。 - jaumebonet
1
我预计字符串比较会比数字比较更昂贵(可能需要更多的内存分配/更大的开销/但不确定)。 - jpp
这是一篇旧帖子,比较字符串与整数的比较,不确定它是否仍然相关。算法似乎完全不同。 - jpp
我明白了。我猜这里的问题是,这是否应该被视为字符串比较还是字符比较(因为我们已经选择了位置),这可能会更快?无论如何,你的解决方案很完美。 - jaumebonet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接