【问题标题】:Why does concatenation of DataFrames get exponentially slower?为什么 DataFrame 的连接会呈指数级变慢?
【发布时间】:2026-02-26 05:45:01
【问题描述】:

我有一个处理 DataFrame 的函数,主要用于将数据处理到存储桶中,使用 pd.get_dummies(df[col]) 在特定列中创建特征的二进制矩阵。

为了避免一次使用此函数处理我的所有数据(内存不足并导致 iPython 崩溃),我使用以下方法将大型 DataFrame 分成块:

chunks = (len(df) / 10000) + 1
df_list = np.array_split(df, chunks)

pd.get_dummies(df) 将根据df[col] 的内容自动创建新列,对于df_list 中的每个df,这些可能会有所不同。

处理后,我使用以下方法将 DataFrame 连接在一起:

for i, df_chunk in enumerate(df_list):
    print "chunk", i
    [x, y] = preprocess_data(df_chunk)
    super_x = pd.concat([super_x, x], axis=0)
    super_y = pd.concat([super_y, y], axis=0)
    print datetime.datetime.utcnow()

第一个块的处理时间是完全可以接受的,但是,它会随着块的增加而增长!这与preprocess_data(df_chunk) 无关,因为它没有理由增加。调用pd.concat() 会导致时间增加吗?

请看下面的日志:

chunks 6
chunk 0
2016-04-08 00:22:17.728849
chunk 1
2016-04-08 00:22:42.387693 
chunk 2
2016-04-08 00:23:43.124381
chunk 3
2016-04-08 00:25:30.249369
chunk 4
2016-04-08 00:28:11.922305
chunk 5
2016-04-08 00:32:00.357365

有没有办法加快这个速度?我有 2900 个块要处理,因此感谢您的帮助!

接受 Python 中的任何其他建议!

【问题讨论】:

    标签: python performance pandas concatenation processing-efficiency


    【解决方案1】:

    永远不要在 for 循环中调用 DataFrame.appendpd.concat。它会导致二次复制。

    pd.concat 返回一个新的 DataFrame。空间必须分配给新的 DataFrame,并且旧 DataFrame 中的数据必须复制到新的 DataFrame 中 数据框。考虑for-loop 中这一行所需的复制量(假设每个x 的大小为1):

    super_x = pd.concat([super_x, x], axis=0)
    
    | iteration | size of old super_x | size of x | copying required |
    |         0 |                   0 |         1 |                1 |
    |         1 |                   1 |         1 |                2 |
    |         2 |                   2 |         1 |                3 |
    |       ... |                     |           |                  |
    |       N-1 |                 N-1 |         1 |                N |
    

    1 + 2 + 3 + ... + N = N(N+1)/2。所以需要O(N**2) 副本 完成循环。

    现在考虑

    super_x = []
    for i, df_chunk in enumerate(df_list):
        [x, y] = preprocess_data(df_chunk)
        super_x.append(x)
    super_x = pd.concat(super_x, axis=0)
    

    Appending to a list is an O(1) operation 并且不需要复制。现在 循环完成后,会调用一次pd.concat。这个呼吁 pd.concat 需要制作 N 个副本,因为 super_x 包含 N 大小为 1 的 DataFrame。因此,以这种方式构造时,super_x 需要 O(N) 副本。

    【讨论】:

    • 嗨@unutbu,感谢您的详细解释,这确实详细解释了理论!
    • 以这种方式(43717、3261)连接 2900 个这种形状的块是否可行?处理步骤现在只需 10 秒。
    • @SantoshGupta7:问题在于速度,而不是内存。无论哪种方式,峰值内存使用量都差不多。当数据帧很大和/或循环执行多次时,复制可能是一个缓慢的操作。制作 O(n^2) 份副本是不必要的慢,因为有一个 O(n) 替代方案 - 附加到列表,循环后连接一次。
    • 将您的解决方案应用到我的程序中包含超过 150 万条数据记录,导致执行时间从 60 多小时缩短到 1 小时以下!我什至明白为什么......! :-) 谢谢!
    • 将此应用到处理 140 万条非常宽的记录的 Kaggle 笔记本上,将执行时间从 9 小时(超时)减少到 25 分钟 - 谢谢!
    【解决方案2】:

    每次连接时,都会返回数据的副本。

    您希望保留一个块列表,然后将所有内容连接起来作为最后一步。

    df_x = []
    df_y = []
    for i, df_chunk in enumerate(df_list):
        print "chunk", i
        [x, y] = preprocess_data(df_chunk)
        df_x.append(x)
        df_y.append(y)
    
    super_x = pd.concat(df_x, axis=0)
    del df_x  # Free-up memory.
    super_y = pd.concat(df_y, axis=0)
    del df_y  # Free-up memory.
    

    【讨论】: