为什么DataFrame的串联操作会呈指数级增加的速度变慢?

11 浏览
0 Comments

为什么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中提出其他建议!

0
0 Comments

为什么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分钟-谢谢!

0
0 Comments

为什么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)

0