【问题标题】:Loop gets slower after each iteration每次迭代后循环变慢
【发布时间】:2020-01-05 12:41:06
【问题描述】:

我有一个python 脚本,内容如下:

  1. 我有一个 json 列表
  2. 我创建了一个空的pandas 数据框
  3. 我在这个列表上运行了一个 for 循环
  4. 我在每次迭代时使用(相同的)键创建一个空字典,这对我来说很有趣
  5. 我在每次迭代时解析 json 以检索键的值
  6. 我在每次迭代时将字典附加到 pandas 数据帧

这样做的问题是,在每次迭代中,处理时间都在增加。 具体来说:

0-1000 documents -> 5 seconds
1000-2000 documents -> 6 seconds
2000-3000 documents -> 7 seconds
...
10000-11000 documents -> 18 seconds
11000-12000 documents -> 19 seconds
...
22000-23000 documents -> 39 seconds
23000-24000 documents -> 42 seconds
...
34000-35000 documents -> 69 seconds
35000-36000 documents -> 72 seconds

为什么会这样?

我的代码如下所示:

# 'documents' is the list of jsons

columns = ['column_1', 'column_2', ..., 'column_19', 'column_20']

df_documents = pd.DataFrame(columns=columns)

for index, document in enumerate(documents):

    dict_document = dict.fromkeys(columns)

    ...
    (parsing the jsons and retrieve the values of the keys and assign them to the dictionary)
    ...

    df_documents = df_documents.append(dict_document, ignore_index=True)

附言

在下面应用@eumiro 的建议后,时间如下:

    0-1000 documents -> 0.06 seconds
    1000-2000 documents -> 0.05 seconds
    2000-3000 documents -> 0.05 seconds
    ...
    10000-11000 documents -> 0.05 seconds
    11000-12000 documents -> 0.05 seconds
    ...
    22000-23000 documents -> 0.05 seconds
    23000-24000 documents -> 0.05 seconds
    ...
    34000-35000 documents -> 0.05 seconds
    35000-36000 documents -> 0.05 seconds

在应用@DariuszKrynicki 的建议后,时间如下:

0-1000 documents -> 0.56 seconds
1000-2000 documents -> 0.54 seconds
2000-3000 documents -> 0.53 seconds
...
10000-11000 documents -> 0.51 seconds
11000-12000 documents -> 0.51 seconds
...
22000-23000 documents -> 0.51 seconds
23000-24000 documents -> 0.51 seconds
...
34000-35000 documents -> 0.51 seconds
35000-36000 documents -> 0.51 seconds
...

【问题讨论】:

  • @JohnColeman, 1) 它可能不同于..., 2) 如果... 是二次的,那么它在每次迭代中不会大致相同?或者如果不一样,它每次都会波动(有时更低,有时更高)?但在这里,正如您所见,它只会稳步增加。
  • 这是因为您的 df_documents 正在增长。
  • @JohnColeman,关于我在下面的第 (1) 点,请参阅下面其他人的答案。 (你删评论就离开战场了?哈哈)

标签: python pandas performance for-loop time


【解决方案1】:

是的,append 到 DataFrame 会在每一行之后变慢,因为它必须一次又一次地复制整个(增长的)内容。

创建一个简单的列表,附加到它,然后一步创建一个DataFrame:

records = []

for index, document in enumerate(documents):
    …
    records.append(dict_document)

df_documents = pd.DataFrame.from_records(records)

【讨论】:

  • 为什么不使用迭代器或生成器?您不需要一直将列表保存在内存中。只有在您想要访问数据时才需要它。
  • 哇,我不知道'追加'仍然如此低效。我已经对其进行了测试,结果要好得多(赞成)。检查我编辑的帖子,谢谢:)
  • @PoeteMaudit DataFrame 是高效的。添加新行也很有效。通过单独附加每一行来创建整个 DataFrame 效率不高。
  • @DariuszKrynicki 这是第 2 级。
【解决方案2】:

答案可能已经存在于您经常使用的pandas.DataFrame.append 方法中。这是非常低效的,因为它需要频繁分配新内存,即复制旧内存,这可以解释您的结果。另见官方pandas.DataFrame.append docs

迭代地将行附加到 DataFrame 可能比单个连接的计算量更大。更好的解决方案是将这些行附加到列表中,然后将列表与原始 DataFrame 一次性连接起来。

两个例子:

效率较低:

>>> df = pd.DataFrame(columns=['A'])
>>> for i in range(5): ...     df = df.append({'A': i}, ignore_index=True)
>>> df    A 0  0 1  1 2  2 3  3 4  4

更高效:

>>> pd.concat([pd.DataFrame([i], columns=['A']) for i in range(5)], ...           ignore_index=True)    A 0  0 1  1 2  2 3  3 4  4

您可以应用相同的策略,创建一个数据帧列表,而不是每次迭代都附加到同一个数据帧,然后在您的for 循环完成后concat

【讨论】:

    【解决方案3】:

    我怀疑您的 DataFrame 会随着每次迭代而增长。 使用迭代器怎么样?

    # documents = # json
    def get_df_from_json(document):
        columns = ['column_1', 'column_2', ..., 'column_19', 'column_20']
        # parsing the jsons and retrieve the values of the keys and assign them to the dictionary)
        # dict_document =  # use document to parse it and create dictionary
        return pd.DataFrame(list(dict_document.values()), index=dict_document)   
    
    res = (get_df_from_json(document) for document in enumerate(documents))
    res = pd.concat(res).reset_index() 
    

    编辑: 我对下面这样的示例进行了快速比较,结果发现使用迭代器并不能加快代码对列表理解的使用:

    import json
    import time
    
    
    def get_df_from_json():
        dd = {'a': [1, 1], 'b': [2, 2]}
        app_json = json.dumps(dd)
        return pd.DataFrame(list(dd.values()), index=dd)
    
    start = time.time()
    res = pd.concat((get_df_from_json() for x in range(1,20000))).reset_index()
    print(time.time() - start)
    
    
    start = time.time()
    res = pd.concat([get_df_from_json() for x in range(1,20000)]).reset_index()
    print(time.time() - start)
    

    迭代器:9.425999879837036 列表理解:8.934999942779541

    【讨论】:

    • 嘿,也谢谢你的回答——看起来很有趣(点赞)。您认为每 1000 个文档 0.05 秒会快多少?我将对其进行测试,但首先要从您的代码中的dd 开始?
    • dd 是我的字典快捷方式。我已经更新了我的答案以引用 dict_document。从内存使用的角度来看,使用迭代器将使您的代码高效。我强烈建议研究 python 中的迭代器和生成器。乍一看它们可能看起来很复杂,但其基本概念很简单且易于使用。
    • 是的,我想到了它们,但我没有广泛使用它们。让我们看看它是否开始更快以及多少。
    • 嘿,我知道了TypeError: get_df_from_json() missing 1 required positional argument: 'document'。我错过了还是你错过了什么?可能是res = (get_df_from_json(document) for document in enumerate(documents))
    • 嘿,在我编辑的帖子中查看时间。它实际上比 eumiro 的解决方案要慢得多。 (除非我错过了什么)