更快的方法来总结数据框中的所有行组合

Posted

技术标签:

【中文标题】更快的方法来总结数据框中的所有行组合【英文标题】:Faster way to sum all combinations of rows in dataframe 【发布时间】:2021-07-13 14:57:37 【问题描述】:

我有一个包含 10,000 行的数据框,我试图将这些行的所有可能组合相加。根据我的数学,这大约是 5000 万个组合。我将举一个小例子来简化我的数据的样子:

df = Ratio     Count     Score
     1         6         11
     2         7         12
     3         8         13
     4         9         14
     5         10        15

这是想要的结果:

results = Min Ratio     Max Ratio     Total Count     Total Score
          1             2             13              23
          1             3             21              36
          1             4             30              50
          1             5             40              65
          2             3             15              25
          2             4             24              39
          2             5             34              54
          3             4             17              27
          3             5             27              42
          4             5             19              29

这是我用来完成计算的代码:

for i in range(len(df)):
    j = i + 1
    while j <= len(df):
        range_to_calc = df.iloc[i:j]
        total_count = range_to_calc['Count'].sum()
        total_score = range_to_calc['Score'].sum()
        new_row = 'Min Ratio': range_to_calc.at[range_to_calc.first_valid_index(),'Ratio'],
                   'Max Ratio': range_to_calc.at[range_to_calc.last_valid_index(),'Ratio'],
                   'Total Count': total_count,
                   'Total Score': total_score
        results = results.append(new_row, ignore_index=True)
        j = j + 1

此代码有效,但根据我的估计,运行几分钟后,需要 200 小时才能完成。我知道使用 numpy 会快很多,但我无法理解如何构建多个数组以相加。 (我认为如果我只做 1+2、2+3、3+4 等会很容易,但要困难得多,因为我需要 1+2、1+2+3、1+2+3 +4 等)是否有更有效的方法来完成此计算,以便它可以在合理的时间内运行?谢谢!

P.S.:如果您想知道我想对 5000 万行数据框做什么,我实际上并不需要在最终结果中使用它。我最终希望将结果中每一行的总分除以其总计数,以获得每个总计数的总分值,然后显示每个总计数的 1,000 个最高总分,以及每个相关的最小比率、最大值比率、总计数和总分。

【问题讨论】:

我不回答你的问题,但你真的需要预先计算和存储所有组合吗?你能不能在需要的时候不使用df[2:4].sum() 之类的东西? 我刚刚编辑了我的帖子以添加 P.S.我认为可以解决您的问题。我最终想比较每一行的值,因为我不知道哪些比率将提供最高的每次计数分数,我认为我能做到这一点的唯一方法是存储所有计算的组合,(或至少存储前 1000并在计算较高的值时剔除最低值。) 所以你不考虑单行作为一个组合? 您已经创建了一个 N=10000 的 N-combinatorics 大小的问题,您绝对需要等待一周才能完成。您可以加快速度,使用 python 矢量化从您的英特尔处理器中提取每个转速,请参阅youtube.com/watch?v=EEUXKG97YRw @mr7 假设你的数据框中有k 条目并且没有负分,最大的分数将是df[1:k].sum().Score,然后检查df[1:k-1].sum().Scoredf[2:k].sum().Score 是否是第二大的可能的总和并重复操作,直到找到最大的 1000 分。它将为您节省一周的计算时间。 【参考方案1】:

经过这些改进后,运行 10k 行需要 ~2 分钟

    对于总和计算,您可以预先计算cumulative sum(cumsum)并保存。 sum(i to j) 等于 sum(0 to j) - sum(0 to i-1)。 现在sum(0 to j)cumsum[j]sum(0 to i - 1)cumsum[i-1]。 所以sum(i to j) = cumsum[j] - cumsum[i - 1]。 这比每次针对不同组合的计算总和都有显着改进。

    numpy 数组上的操作比 pandas 系列上的操作更快,因此将每个列转换为 numpy 数组,然后对其进行计算。

    (来自其他答案):不是在列表中追加,而是初始化一个大小为((n*(n+1)//2) -n , 4) 的空numpy数组并使用它来保存结果。

用途:

count_cumsum = np.cumsum(df.Count.values)
score_cumsum = np.cumsum(df.Score.values)
ratios = df.Ratio.values
n = len(df)
rowInCombination = (n * (n + 1) // 2) - n
arr = np.empty(shape = (rowInCombination, 4), dtype = int)
k = 0
for i in range(len(df)):
    for j in range(i + 1, len(df)):
        arr[k, :] = ([
              count_cumsum[j] - count_cumsum[i-1] if i > 0 else count_cumsum[j], 
              score_cumsum[j] - score_cumsum[i-1] if i > 0 else score_cumsum[j],
              ratios[i],
              ratios[j]])
        k = k + 1
out = pd.DataFrame(arr, columns = ['Total_Count', 'Total_Score', 
                    'Min_Ratio', 'Max_Ratio'])

输入:

df = pd.DataFrame('Ratio': [1, 2, 3, 4, 5], 
                   'Count': [6, 7, 8, 9, 10],
                   'Score': [11, 12, 13, 14, 15])

输出:

>>>out

  Min_Ratio Max_Ratio   Total_Count Total_Score
0   1     2              13                 23
1   1     3              21                 36
2   1     4              30                 50
3   1     5              40                 65
4   2     3              15                 25
5   2     4              24                 39
6   2     5              34                 54
7   3     4              17                 27
8   3     5              27                 42
9   4     5              19                 29

【讨论】:

@AmitVikramsign 不错! +1 我可以提出提高速度的建议吗?使用 numpy 像 count_cumsum = np.cumsum(df['Count'].to_numpy())score_cumsum = np.cumsum(df['Score'].to_numpy()) 这样的 cumsum 按照我的时间安排将时间减半。 @ScottBoston 不错的建议。 numpy 数组上的操作比 pandas 系列操作更快。 .我已经更新了我的答案。 @ScottBoston 这里似乎真正的瓶颈是在每次迭代中构造 new_row dict。 我试图通过使用 itertools 组合来消除双循环,但这没有帮助:) 看起来列表理解可以给我们 10 倍的改进。我更新了我的答案。【参考方案2】:

首先,您可以改进算法。然后,您可以使用 Numpy 矢量化/广播加快计算速度。

以下是提高算法性能的有趣点:

Pandas 的append 很慢,因为它重新创建了一个新的数据框。你永远不应该在代价高昂的循环中使用它。相反,您可以将这些行附加到 Python 列表中,甚至直接将项目写入预先分配的 Numpy 向量中。 计算部分和需要 O(n) 时间,而您可以预先计算累积和,然后在恒定时间内找到部分和。 CPython 循环非常慢,但由于广播,内部循环可以使用 Numpy 进行矢量化。

这是生成的代码:

import numpy as np
import pandas as pd

def fastImpl(df):
    n = len(df)
    resRowCount = (n * (n+1)) // 2
    k = 0

    cumCounts = np.concatenate(([0], df['Count'].astype(int).cumsum()))
    cumScores = np.concatenate(([0], df['Score'].astype(int).cumsum()))
    ratios = df['Ratio'].astype(int)
    minRatio = np.empty(resRowCount, dtype=int)
    maxRatio = np.empty(resRowCount, dtype=int)
    count = np.empty(resRowCount, dtype=int)
    score = np.empty(resRowCount, dtype=int)

    for i in range(n):
        kStart, kEnd = k, k+(n-i)
        jStart, jEnd = i+1, n+1
        minRatio[kStart:kEnd] = ratios[i]
        maxRatio[kStart:kEnd] = ratios[i:n]
        count[kStart:kEnd] = cumCounts[jStart:jEnd] - cumCounts[i]
        score[kStart:kEnd] = cumScores[jStart:jEnd] - cumScores[i]
        k = kEnd
    assert k == resRowCount

    return pd.DataFrame(
        'Min Ratio': minRatio,
        'Max Ratio': maxRatio,
        'Total Count': count,
        'Total Score': score
    )

请注意,此代码给出的结果与您问题中的代码相同,但原始代码并未给出问题中所述的预期结果。另请注意,由于输入是整数,为了性能,我强制 Numpy 使用整数(尽管该算法也应该使用浮点数)。

此代码比大数据帧上的原始代码快数十万倍,它成功地在 0.7 秒内计算出 10,000 行的数据帧

【讨论】:

感谢您的贡献!有空我会测试一下。 太棒了!想不出更好的解决方案。【参考方案3】:

其他人已经解释了为什么你的算法这么慢,所以我会深入研究。

让我们对您的问题采取不同的方法。具体来说,看看Total CountTotal Score 列是如何计算的:

计算从 1 到 n 每一行的累积和 计算从 2 到 n 每一行的累积和 ... 计算从 n 到 n 每一行的累积和

由于累加和是累加的,我们只需要对第1行到第n行计算一次:

(2到n)的cumsum是(1到n)-(第1行)的cumsum (3 到 n) 的 cumsum 是 (2 to n) - (row 2) 的 cumsum 等等……

换句话说,当前的 cumsum 是前一个 cumsum 减去它的第一行,然后删除第一行。


正如您所推测的那样,pandas 比 numpy 慢很多,因此我们将一切都转换为 numpy 以提高速度:

arr = df[['Ratio', 'Count', 'Score']].to_numpy() # Convert to numpy array

tmp = np.cumsum(arr[:, 1:3], axis=0)       # calculate cumsum for row 1 to n
tmp = np.insert(tmp, 0, arr[0, 0], axis=1) # create the Min Ratio column
tmp = np.insert(tmp, 1, arr[:, 0], axis=1) # create the Max Ratio column

results2 = [tmp]
for i in range(1, len(arr)):
    tmp = results2[-1][1:] # current cumsum is the previous cumsum without the first row
    diff = results2[-1][0] # the previous cumsum's first row

    tmp -= diff            # adjust the current cumsum
    tmp[:, 0] = arr[i, 0]  # new Min Ratio
    tmp[:, 1] = arr[i:, 0] # new Max Ratio
    results2.append(tmp)

# Assemble the result
results2 = np.concatenate(results2).reshape(-1,4)
results2 = pd.DataFrame(results2, columns=['Min Ratio', 'Max Ratio', 'Total Count', 'Total Score'])

在我的测试期间,这会在大约 2 秒内产生 10k 行数据帧的结果。

【讨论】:

感谢您的解释!这是非常聪明的想法,我从来没有想过!【参考方案4】:

抱歉,这个主题写得晚了,但我只是在寻找类似主题的解决方案。这个问题的解决方案很简单,因为组合只是成对的。这可以通过将数据帧上传到任何数据库并执行以下持续时间小于 10 秒的查询来解决:

SEL f1.*,f2.*,f1.score+f2.score 
FROM table_with_data_source f1, table_with_data_source f2
where f1.ratio<>f2.ratio;

即使有 100,000 条或更多记录,数据库也会很快完成。

但是,我在答案中看到的所有算法都没有真正执行值的组合。他只成对做。当它是一个真正的组合时,问题就变得复杂了,例如:

给定:a、b、c、d 和 e 作为记录:

a
b
c
d
e

真正的组合是:

a+b
a+c
a+d
a+e
a+b+c
a+b+d
a+b+e
a+c+d
a+c+e
a+d+e
a+b+c+d
a+b+c+e
a+c+d+e
a+b+c+d+e
b+c
b+d
b+e
b+c+d
b+c+e
b+d+e
c+d
c+e
c+d+e
d+e

这是一个真正的组合,它涵盖了所有可能的组合。对于这种情况,我无法找到合适的解决方案,因为它确实会影响任何硬件的性能。任何人都知道如何使用 python 执行真正的组合?在数据库级别,它会影响数据库的总体性能。

【讨论】:

以上是关于更快的方法来总结数据框中的所有行组合的主要内容,如果未能解决你的问题,请参考以下文章

聚合火花数据框中的多列(所有组合)

在 R 中组合大量数据集的更快方法?

将特定行加载为组合框中的默认项

根据组合框中的是/否值逐行更新表格列

从组合框中获取数据并使用它来获取另一个组合框中的数据

使用 Combobox 控制表单