为什么DataFrame的串联操作会呈指数级增加的速度变慢?
为什么DataFrame的串联操作会呈指数级增加的速度变慢?
我有一个处理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
,这些列可能不同。
处理完后,我使用以下代码将DataFrames连接起来:
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中提出其他建议!
为什么DataFrame的拼接操作会呈指数级增加时间?
在for循环内部调用DataFrame.append或pd.concat会导致二次拷贝,从而使得拼接操作的时间复杂度呈现二次增长。pd.concat返回一个新的DataFrame,需要为新的DataFrame分配空间,并将旧的DataFrame的数据拷贝到新的DataFrame中。考虑下面这行代码在for循环内部的拷贝操作(假设每个x的大小为1):
super_x = pd.concat([super_x, x], axis=0)
| 迭代次数 | 旧super_x的大小 | x的大小 | 拷贝次数 |
| 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)
将数据块追加到列表是一个O(1)的操作,不需要拷贝。此时,在循环结束后只需要一次pd.concat的调用。这次调用需要进行N次拷贝操作,因为super_x包含了N个大小为1的DataFrame。因此,用这种方式构建super_x只需要O(N)次拷贝操作。
问题出在速度上,而不是内存上。两种方式的峰值内存使用量大致相同。当DataFrame很大或者循环次数较多时,拷贝操作可能会很慢。进行O(n^2)次拷贝操作是不必要的,因为有一种O(n)的替代方案-将数据追加到列表中,在循环结束后进行一次拼接。
将这种解决方案应用于我的程序中,其中有超过150万条数据记录,执行时间从60多小时减少到不到1小时!而且我现在也明白了为什么会这样!:-) 谢谢!
在一个Kaggle笔记本上运行这个方法,对于1.4m非常宽的记录,将执行时间从超过9小时(超时)降低到25分钟-谢谢!
为什么DataFrame的拼接操作会变得指数级别地变慢?
在Python等高级语言中,手动管理内存是一种不好的做法,因为实际上你无法像在C语言中那样管理内存。当你使用del
删除一个变量时,实际上是删除了对这个变量的绑定(参考docs.python.org/3.10/reference/…中的第三段)。稍后的垃圾回收器可能会释放内存,但具体是何时以及释放多少内存取决于垃圾回收算法(这是相当复杂的)。
每次进行拼接操作时,都会返回数据的一个副本。为了解决这个问题,你需要将数据块存储在一个列表中,然后在最后一步将它们拼接起来。
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 # 释放内存。 super_y = pd.concat(df_y, axis=0) del df_y # 释放内存。
我只会在数据占用大量内存或可用内存有限时才删除变量。此外,重新赋值比删除更容易,例如df_x = pd.concat(df_x, axis=0)
。