如何加速 pandas 循环?

How to speed up a pandas loop?

我有一个名为 'matrix' 的 pandas 数据框,它看起来像这样:

    antecedent_sku consequent_sku similarity
0   001            002            0.3
1   001            003            0.2
2   001            004            0.1
3   001            005            0.4
4   002            001            0.4
5   002            003            0.5
6   002            004            0.1

在这个数据框之外,我想创建一个相似矩阵以进一步聚类。我分两步完成。

第 1 步:创建一个空的相似度矩阵 ('similarity')

set_name = set(matrix['antecedent_sku'].values) 
similarity = pd.DataFrame(index = list(set_name), columns = list(set_name))

第 2 步:用 'matrix':

中的值填充它
for ind in tqdm(list(similarity.index)):
        
     for col in list(similarity.columns):
            
            if ind==col:
                similarity.loc[ind, col] = 1
                
            elif len(matrix.loc[(matrix['antecedent_sku'].values==f'{ind}') & (matrix['consequent_sku'].values==f'{col}'), 'similarity'].values) < 1:
                similarity.loc[ind, col] = 0
                
            else:
                similarity.loc[ind, col] = matrix.loc[(matrix['antecedent_sku'].values==f'{ind}') & (matrix['consequent_sku'].values==f'{col}'), 'similarity'].values[0]

问题:填充形状为 (3000,3000) 的矩阵需要 4 个小时。

问题:我做错了什么?我的目标是用 Cython/Numba 之类的东西来加速代码,还是问题出在我的方法的架构上,我应该使用内置函数或其他一些聪明的方法将 'matrix' 转换为 'similarity' 而不是双循环?

P.S。我运行Python3.8.7

众所周知,使用 loc 遍历 pandas 数据帧非常慢。众所周知,CPython 解释器也很慢(通常是循环)。每个 pandas 操作都有很高的开销。但是,要点是您迭代了 3000x3000 个元素,以便为每个元素调用诸如 matrix['antecedent_sku'].values==f'{ind}' 之类的东西,它肯定会迭代 3000 个项目,这些项目是字符串,也被认为是一种低效的数据类型(因为处理器需要解析一个 variable-length UTF-8 多字符序列)。由于每次迭代执行两次并且您为每次比较解析一个新整数,这意味着将执行 3000*3000*3000*2 = 54_000_000_000 个字符串比较,总共 3000*3000*3000*2*2*3 = 324_000_000_000 个字符进行(低效)比较! 这不可能很快,因为效率很低。更不用说每个 9_000_000 迭代 creates/delete 几个临时数组和 Pandas 对象。

首先要做的是由于一些预计算减少重新计算操作的数量。实际上,您可以将 matrix['antecedent_sku'].values==f'{ind}' 的值(因为 Numpy 数组,因为 pandas 系列效率低下)存储在由 ind 索引的字典中,以便在循环中更快地获取它。这应该使这部分快 3000 倍(因为应该只有 3000 个项目)。更好的是:您可以使用 groupby 来更有效地做到这一点。

此外,您可以将列转换为整数(即antecedent_skuconsequent_sku)以避免许多昂贵的字符串比较。

然后你可以像matrix.loc[..., 'similarity'].values一样删除无用的操作。事实上,由于你只想知道结果的长度,你可以只使用二进制 numpy 数组的 np.sum 。事实上,你甚至可以使用 np.any 因为你检查长度是否小于 1.

然后您可以避免使用预分配缓冲区创建临时 Numpy 数组,并通过在 Numpy 操作中指定输出缓冲区。例如,您可以使用 np.logical_and(A, B, out=your_preallocated_buffer) 而不仅仅是 A & B.

最后,如果(且仅当)所有前面的步骤都不足以使整体计算速度提高数百或数千倍,您可以通过先将数据帧转换为 Numpy 数组来使用 Numba(因为 Numba 不支持数据框)。如果这还不够,您可以使用 prange(而不是 range)和 Numba 的标志 parallel=True 以便使用多线程。

请注意 Pandas 并不是真正设计用于操作 3000 列的数据帧,因此肯定不会很快。 Numpy 更适合处理矩阵。

在 Jerome 的指导下,我完成了以下工作:

第一步:创建字典

matrix_dict = matrix.copy()
matrix_dict = matrix_dict.set_index(['antecedent_sku', 'consequent_sku'])['similarity'].to_dict()

matrix_dict 看起来像这样:

{(001, 002): 0.3}

第 2 步:用 matrix_dict

中的值填充相似度
for ind in tqdm(list(similarity.index)):
        
     for col in list(similarity.columns):
            
            if ind==col:
                similarity.loc[ind, col] = 1
                
            else:

                similarity.loc[ind, col] = matrix_dict.get((int(ind), int(col)))

第 3 步:用零填充

similarity = similarity.fillna(0)

结果:x35 性能(4 小时 20 分钟到 7 分钟)