为什么DataFrame的连接速度会成倍增长?

jfi*_*ive 27 python performance concatenation processing-efficiency pandas

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

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

chunks = (len(df) / 10000) + 1
df_list = np.array_split(df, chunks)
Run Code Online (Sandbox Code Playgroud)

pd.get_dummies(df)会自动创建一个基于内容的新栏目df[col]和这些都有可能为每个不同dfdf_list.

处理完毕后,我使用以下方法将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()
Run Code Online (Sandbox Code Playgroud)

第一个块的处理时间是完全可以接受的,然而,它每块增长!这与它没有关系,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
Run Code Online (Sandbox Code Playgroud)

是否有解决方法来加快速度?我有2900个块要处理,所以任何帮助表示赞赏!

在Python中打开任何其他建议!

unu*_*tbu 39

永远不要打电话DataFrame.appendpd.concat在for-loop里面.它导致二次复制.

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 |
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

附加到列表是一项O(1)操作,不需要复制.现在pd.concat循环完成后只有一个调用.此调用 pd.concat需要创建N个副本,因为super_x包含N 大小为1的DataFrame.因此,当以这种方式构造时,super_x需要O(N) 副本.

  • 将您的解决方案应用到我的包含超过 150 万条数据记录的程序中,执行时间从 60 多个小时缩短到不到 1 小时!我什至明白为什么......!:-) 谢谢! (3认同)
  • 嗨@unutbu,感谢您的详细解释,这确实详细解释了理论! (2认同)
  • @ SantoshGupta7:问题在于速度,而不是内存。两种方式的峰值内存使用情况大致相同。当数据帧较大和/或执行多次循环时,复制可能会很慢。制作O(n ^ 2)副本的过程不必要地很慢,因为存在O(n)替代方案-追加到列表中,在循环后进行一次concat。 (2认同)
  • 将此应用于处理 1.4m 非常宽记录的 Kaggle 笔记本,将执行时间从超过 9 小时(超时)减少到 25 分钟 - 谢谢! (2认同)

Ale*_*der 7

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

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

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.
Run Code Online (Sandbox Code Playgroud)

  • 尝试在 Python 等高级语言中手动管理内存是一种不好的做法,因为实际上您无法像 C 那样管理内存。当你`del`一个变量时会发生什么,你[删除绑定](https://docs.python.org/3.10/reference/simple_stmts.html#the-del-statement)(第三段)。后来的垃圾收集器_可能_释放内存,但是何时以及释放多少量取决于GC算法(这是相当复杂的)。 (2认同)