Pandas.groupby.apply() 中的内存泄漏问题?

10

我目前正在使用Pandas处理大约600MB的csv源文件进行一个项目。在分析过程中,我将csv读入数据帧,按一些列进行分组,然后对分组的数据帧应用简单的函数。我注意到在这个过程中进入了Swap Memory,所以进行了一个基本测试:

我首先在shell中创建了一个相当大的数据帧:

import pandas as pd
import numpy as np
df = pd.DataFrame(np.random.randn(3000000, 3),index=range(3000000),columns=['a', 'b', 'c'])

我定义了一个毫无意义的函数,它叫做do_nothing():

def do_nothing(group):
    return group

然后运行了以下命令:

df = df.groupby('a').apply(do_nothing)

我的系统有16gb的RAM,运行的是Debian (Mint)。在创建了数据框之后,我正在使用大约600mb的RAM。一旦开始执行apply方法,该值就开始飙升。它稳步上升到约7gb(!),然后完成命令并回落到5.4gb(而shell仍处于活动状态)。问题是,我的工作需要执行更多的操作而不仅仅是“do_nothing”方法,因此当执行真正的程序时,我会限制我的16gb RAM,并开始交换,使程序无法使用。这是故意的吗?我看不出Pandas为什么要使用7gb的RAM来有效地执行“do_nothing”,即使它必须存储分组对象。

有什么想法是什么导致这个问题/如何修复它吗?

谢谢,

.P


这不一定是内存泄漏;它可能只是使用大量临时存储并在最后正确释放的算法。(Python通常不会将内存释放回操作系统。但即使它这样做,也无济于事,因为你关心的是峰值使用率。) - abarnert
好观点。我无法想象Pandas可能采用什么算法需要这么多临时存储空间。我猜我需要找到一个Pandas专家,如果确实是一种意图,并且只是方法的副产品,那么他们可以建议一个解决方法。 - user3908739
那个例子有点病态。 groupby为每个不同的值创建一个独立的组。由于您生成的值是随机浮点数,它们很可能都是不同的,这意味着有3百万个组。传递给do_nothing的每个组都是DataFrame,因此您正在创建3百万个DataFrames(然后apply必须将其聚合成单个结果)。即使每个仅有一行,这也是很多开销。创建一个“分组性”(即不同分组的数量)与实际数据更符合的示例可能更加明显。 - BrenBarn
我同意,这有点虐待狂的味道。所以你是说一个包含3列和2行的数据框比一个包含3列和1行的数据框小不到两倍?这可能是真的(老实说我不确定Pandas的后端是如何处理的)。我的项目中的数据平均每组有大约3-5行,“groupiness”的程度比较高,来自一个大约有250万行的数据框。因此,虽然我同意使用更粗略的分组可能会有所帮助,但在这个特定的项目中并不是很可行。 - user3908739
@user3908739:是的,DataFrames是Python对象,并且与它们相关联的开销;它们不能只存储其数据的原始字节而没有任何包装。因此,许多微小的DataFrames将占用比几个大型DataFrame更多的内存。话虽如此,您报告的内存使用情况似乎相当高。我也不是pandas内部专家,所以如果有人来了解如何管理这个问题,也许您可以获得更多的见解。出于好奇,如果您使do_nothing返回1而不是group,会发生什么? - BrenBarn
2个回答

13

使用0.14.1版本,我认为没有内存泄漏问题(仅占据你的框架大小的三分之一)。

In [79]: df = DataFrame(np.random.randn(100000,3))

In [77]: %memit -r 3 df.groupby(df.index).apply(lambda x: x)
maximum of 3: 1365.652344 MB per loop

In [78]: %memit -r 10 df.groupby(df.index).apply(lambda x: x)
maximum of 10: 1365.683594 MB per loop

如何解决这样的问题,有两点一般性建议:

1)尽可能使用Cython级别函数,这将会更快,占用的内存也会更少。换句话说,如果可能的话,将分组表达式与函数分离开来并避免使用函数是非常值得的(当然,某些情况可能太过复杂,但关键是要拆分问题)。例如:

不要这样写:

df.groupby(...).apply(lambda x: x.sum() / x.mean())

最好做:

g = df.groupby(...)
g.sum() / g.mean()

2) 你可以通过手动进行聚合来轻松“控制”分组,这样还可以实现定期输出和垃圾收集(如果需要的话)。

results = []
for i, (g, grp) in enumerate(df.groupby(....)):

    if i % 500 == 0:
        print "checkpoint: %s" % i
        gc.collect()


    results.append(func(g,grp))

# final result
pd.concate(results)

非常棒的回答。非常感谢! - user3908739
1
在每n百行上使用迭代运行gc.collect()似乎不应该比.apply()节省那么多内存,但尽管如此,这种方法对我来说产生了巨大的影响。这是我能够让pandas在处理大型数据框时不被杀死以使用过多RAM的唯一方法。另请参见:https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ - npdoty
你是指concat还是concate? - Nathan B
结果对象是Series,而不是Dataframe。 - Nathan B

0

我的解决方案:

result_df = None
        for i, (account_id, grp) in enumerate(grouped_df):
            grp.name = account_id
            if i % 500 == 0:
                print(f"\rStep {i}", end="", flush=True)
                gc.collect()

            series = partial_func(grp)
            if (
                series is not None
            ):  
                dataframed = series.to_frame().transpose()

                if result_df is None:
                    result_df = dataframed
                else:
                    result_df.append(dataframed)

            else:
                print("Cleaning dropped row.")

       
        grouped_df = result_df
        del result_df
        gc.collect()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接