你将如何优化这个简短但非常慢的 Python 循环?

Posted

技术标签:

【中文标题】你将如何优化这个简短但非常慢的 Python 循环?【英文标题】:How would you optimize this short but very slow Python loop? 【发布时间】:2020-05-01 13:43:21 【问题描述】:

我正在从 R 切换到 Python。不幸的是,我发现虽然某些结构在 R 中几乎可以立即运行,但在 Python 中却需要几秒钟(甚至几分钟)。阅读后,我发现在 pandas 中强烈建议不要使用 for 循环,建议使用其他替代方法,例如矢量化和应用。

在此示例代码中:从从最小值到最大值排序的一列值中,保留在长度为“200”的间隙之后出现的所有值。

import numpy as np
import pandas as pd

#Let's create the sample data. It consists of a column with random sorted values, and an extra True/False column, where we will flag the values we want
series = np.random.uniform(1,1000000,100000)
test = [True]*100000
data = pd.DataFrame('series' : series, 'test':test )
data.sort_values(by=['series'], inplace=True)

#Loop to get rid of the next values that fall within the '200' threshold after the first next valid value
for i in data['series']:
    if data.loc[data['series'] == i,'test'].item() == True:
        data.loc[(data['series'] > i) & (data['series'] <= i+200  ) ,'test' ] = False
#Finally, let's keep the first values after any'200' threshold             
data = data.loc[data['test']==True , 'series']

是否可以将其转换为函数、矢量化、应用或除“for”循环之外的任何其他结构以使其几乎立即运行?

【问题讨论】:

对于这种动态,for 循环似乎是不可避免的。 注意,pandas 中的.apply 不会比循环快,除非您在列上应用矢量化函数。 我认为问题出在data['series'] == i,这是一个 O(n) 操作,data['series'] &gt; idata['series'] &lt;= i+200 也是 O(n)。所以你有一个外部循环(for i in data['series']),它将运行 O(n) 次,并且在循环内你正在执行 O(n) 操作。所以你的算法是O(n ^ 2)。由于输入的大小为 100,000,因此您将执行大约 10^10 次操作,这很棘手。 补充我上面的评论:我猜 R 中的相应算法在排序后只是 O(n),所以它可能运行得更快。 (O(n) 算法将遍历列的所有索引i,并检查 i 和 i+1 处的值之间的差异) 【参考方案1】:

您可以通过一个简单的单遍算法在系列上使用一个循环来做到这一点;不需要矢量化或类似的东西。在我的机器上需要 33 毫秒,所以不是“瞬时”,而是眨眼,你会错过它。

def first_after_gap(series, gap=200):
    out = []
    last = float('-inf')
    for x in series:
        if x - last >= gap:
            out.append(x)
            last = x
    return out

例子:

>>> import numpy as np
>>> series = sorted(np.random.uniform(1, 1000000, 100000))
>>> from timeit import timeit
>>> timeit(lambda: first_after_gap(series), number=1)
0.03264855599991279

【讨论】:

IIUC,这与 OP 的预期有些不同。这只是将系列中的每个元素与前一个元素进行比较,这是完全可矢量化的。也许将last = x 放入if x - last &gt;= gap 我确定它是可向量化的,我只是说没有必要优化它,因为简单的实现足够高效(与“秒或分钟”相比)。行为上的具体区别是什么? 我想说的是,它不是 OP想要的。 我明白了,我问行为上有什么不同?不过,我看到您编辑了您的评论以建议进行更改。我已经编辑了答案并更新了时间测量。 对不起,我错过了那部分。你的方法实际上比我的更干净。不同之处在于,人们不知道需要保留/比较哪些值,因此无法向量化此解决方案。【参考方案2】:

这是我使用while 循环的方法:

head = 0
indexes = []
while head < len(data):
    thresh = data['series'].iloc[head] + 200
    indexes.append(head)
    head += 1
    while head < len(data) and data['series'].iloc[head] < thresh:
        head+=1

# output:
data = data.iloc[indexes]

# double check with your approach
set(data.loc[data['test']].index) == set(data.iloc[indexes].index)
# output: True

上述方法耗时 984 毫秒,而您的方法耗时 56 秒。

【讨论】:

这就完成了。谢谢。我将尝试将此结构用于类似的任务。不幸的是,我的循环需要很多子集。子集太多和附加数据可能会非常缓慢。你会给我什么建议?【参考方案3】:

searchsorted

您可以找到下一个,而无需遍历所有...。 这应该更快。 正如 cmets 中所指出的,更快取决于数据。

请注意,我使用与 Quang 类似的方法,因为它们是正确的,您必须循环。不同之处在于我使用searchsorted 来查找每个位置的下一个位置,而不是循环遍历每个位置并评估是否应该添加该位置。

a = data.series.to_numpy()
head = 0
indexes = [head]
while head < len(data):
    head = a[head:].searchsorted(a[head] + 200) + head
    if -1 < head < len(data):
        indexes.append(head)

data.iloc[indexes]

              series  test
77193       5.663829  True
36166     210.829727  True
85730     413.206840  True
68686     613.849315  True
88026     819.096379  True
...              ...   ...
13863  999074.688286  True
31992  999276.058929  True
71844  999487.746496  True
84515  999690.104536  True
6029   999891.101087  True

[4761 rows x 2 columns]

【讨论】:

searchsorted 是 O(log(n)),使用 while 循环可能不会比普通线性循环快。真的取决于数据。 你认为什么样的数据会使这个速度变慢?你让我很好奇(-: 也许你必须添加每个位置? 我正想说完全一样的:-)。 我将阈值更改为0.1 而不是(200),它仍然支持我的方法。但我部分相信你的观点。所以现在我要在这上面浪费太多时间了... thx (-:

以上是关于你将如何优化这个简短但非常慢的 Python 循环?的主要内容,如果未能解决你的问题,请参考以下文章

回流/重绘问题?优化太慢的应用

非常慢的循环PHP

Python 三级菜单与优化(一层循环嵌套)

非常慢的 MySQL COUNT DISTINCT 查询,即使有索引——如何优化?

使用 for 循环和过滤器优化代码

MySQL 非常慢的循环