创建pandas dataframe时内存使用量增加

4
我有一段代码,它从另一个函数接收回调并创建一个列表(pd_arr)。然后使用该列表创建数据帧。最后删除列表的列表。
在使用内存分析器进行分析时,输出如下:
102.632812 MiB   0.000000 MiB       init()
236.765625 MiB 134.132812 MiB           add_to_list()
                                    return pd.DataFrame()
394.328125 MiB 157.562500 MiB       pd_df = pd.DataFrame(pd_arr, columns=df_columns)
350.121094 MiB -44.207031 MiB       pd_df = pd_df.set_index(df_columns[0])
350.292969 MiB   0.171875 MiB       pd_df.memory_usage()
350.328125 MiB   0.035156 MiB       print sys.getsizeof(pd_arr), sys.getsizeof(pd_arr[0]), sys.getsizeof(pd_df), len(pd_arr)
350.328125 MiB   0.000000 MiB       del pd_arr

在检查pd_df(数据框)的深层内存使用情况时,它为80.5 MB。所以我的问题是,在del pd_arr行之后,为什么内存没有减少。

此外,根据分析器的总数据框大小(157-44 = 110 MB),似乎比80 MB还多。那是什么导致了差异?

另外,是否有其他内存高效的方法来创建数据框(在循环中接收数据),而时间性能又不太差(例如:100MB大小的数据框增量应该在10秒左右)?

编辑:简单的Python脚本解释了这种行为

Filename: py_test.py

Line #    Mem usage    Increment   Line Contents
================================================
     9    102.0 MiB      0.0 MiB   @profile
    10                             def setup():
    11                              global arr, size
    12    102.0 MiB      0.0 MiB    arr = range(1, size)
    13    131.2 MiB     29.1 MiB    arr = [x+1 for x in arr]


Filename: py_test.py

Line #    Mem usage    Increment   Line Contents
================================================
    21    131.2 MiB      0.0 MiB   @profile
    22                             def tearDown():
    23                              global arr
    24    131.2 MiB      0.0 MiB    del arr[:]
    25    131.2 MiB      0.0 MiB    del arr
    26     93.7 MiB    -37.4 MiB    gc.collect()

介绍DataFrame:

Filename: py_test.py

Line #    Mem usage    Increment   Line Contents
================================================
     9    102.0 MiB      0.0 MiB   @profile
    10                             def setup():
    11                              global arr, size
    12    102.0 MiB      0.0 MiB    arr = range(1, size)
    13    132.7 MiB     30.7 MiB    arr = [x+1 for x in arr]


Filename: py_test.py

Line #    Mem usage    Increment   Line Contents
================================================
    15    132.7 MiB      0.0 MiB   @profile
    16                             def dfCreate():
    17                              global arr
    18    147.1 MiB     14.4 MiB    pd_df = pd.DataFrame(arr)
    19    147.1 MiB      0.0 MiB    return pd_df


Filename: py_test.py

Line #    Mem usage    Increment   Line Contents
================================================
    21    147.1 MiB      0.0 MiB   @profile
    22                             def tearDown():
    23                              global arr
    24                              #del arr[:]
    25    147.1 MiB      0.0 MiB    del arr
    26    147.1 MiB      0.0 MiB    gc.collect()

1
你确定代码中没有其他地方引用了 pd_arr 吗?Python 是基于引用计数的,所以使用 del 只有在确保已删除的对象不会从任何地方使用时才能释放相关内存。你也可以尝试清空列表 - jdehesa
我尝试使用 del pd_arr[:]。没有减少内存。在代码中,pd_arr被定义为全局变量。这会有所不同吗? - Rajs123
好吧,del pd_arr 只是意味着你不能再使用 pd_arr 这个名称来引用那个列表了,无论是全局的还是局部的。但是,如果在之前的某个地方有类似 a = pd_arr 的操作(尽管可能更加微妙,比如将 pd_arr 传递给一个函数并在其他地方复制其引用),那么它实际上并没有被真正删除。然而,我无法解释为什么 del pd_arr[:] 没有任何区别。 - jdehesa
没有其他指向pd_arr的引用指针。 - Rajs123
1个回答

4
回答你的第一个问题,当你尝试使用“del pd_arr”清除内存时,实际上并不会发生这种情况,因为DataFrame存储了一个到pd_arr的链接,并且顶级作用域保留了一个更多的链接;减少引用计数器不会收集内存,因为这个内存正在使用中。
您可以通过在运行del pd_arr之前运行sys.getrefcount(pd_arr)来检查我的假设,您将得到2作为结果。
现在,我相信以下代码段做同样的事情,就像你试图做的一样: https://gist.github.com/vladignatyev/ec7a26b7042efd6f710d436afbfb87de/90df8cc6bbb8bd0cb3a1d2670e03aff24f3a5b24 如果您尝试运行此代码段,则会看到内存使用情况如下:
Line #    Mem usage    Increment   Line Contents
================================================
    13   63.902 MiB    0.000 MiB   @profile
    14                             def to_profile():
    15  324.828 MiB  260.926 MiB       pd_arr = make_list()
    16                                 # pd_df = pd.DataFrame.from_records(pd_arr, columns=[x for x in range(0,1000)])
    17  479.094 MiB  154.266 MiB       pd_df = pd.DataFrame(pd_arr)
    18                                 # pd_df.info(memory_usage='deep')
    19  479.094 MiB    0.000 MiB       print sys.getsizeof(pd_arr), sys.getsizeof(pd_arr[0])
    20  481.055 MiB    1.961 MiB       print sys.getsizeof(pd_df), len(pd_arr)
    21  481.055 MiB    0.000 MiB       print sys.getrefcount(pd_arr)
    22  417.090 MiB  -63.965 MiB       del pd_arr
    23  323.090 MiB  -94.000 MiB       gc.collect()

试试这个例子:

@profile
def test():
    a = [x for x in range(0,100000)]
    del a


aa = test()

您会得到您所期望的精确内容:
Line #    Mem usage    Increment   Line Contents
================================================
     6   64.117 MiB    0.000 MiB   @profile
     7                             def test():
     8   65.270 MiB    1.152 MiB       a = [x for x in range(0,100000)]
     9                                 # print sys.getrefcount(a)
    10   64.133 MiB   -1.137 MiB       del a
    11   64.133 MiB    0.000 MiB       gc.collect()

此外,如果您调用 sys.getrefcount(a),有时会在执行 del a 前清理内存:
Line #    Mem usage    Increment   Line Contents
================================================
     6   63.828 MiB    0.000 MiB   @profile
     7                             def test():
     8   65.297 MiB    1.469 MiB       a = [x for x in range(0,100000)]
     9   64.230 MiB   -1.066 MiB       print sys.getrefcount(a)
    10   64.160 MiB   -0.070 MiB       del a

但是当你使用pandas时,情况会变得疯狂起来。

如果您打开pandas.DataFrame的源代码,您会发现,在使用list初始化DataFrame的情况下,pandas会创建一个新的NumPy数组并复制其内容。请看这个:https://github.com/pandas-dev/pandas/blob/master/pandas/core/frame.py#L329

删除pd_arr不会释放内存,因为pd_arr将在DataFrame创建和退出您的函数之后被收集,因为它没有任何其他链接。在此之前和之后调用getrefcount证明了这一点。

从普通列表创建新的DataFrame会将您的列表复制到NumPy数组中。(查看np.array(data, dtype=dtype, copy=copy)以及有关array的相应文档) 复制操作可能会影响执行时间,因为分配新的内存块是一个繁重的操作。

我已经尝试使用Numpy数组初始化新的DataFrame。唯一的区别是numpy.Array内存开销出现的位置。比较以下两个代码片段:

def make_list():  # 1
    pd_arr = []
    for i in range(0,10000):
        pd_arr.append([x for x in range(0,1000)])
    return np.array(pd_arr)

并且

def make_list():  #2
    pd_arr = []
    for i in range(0,10000):
        pd_arr.append([x for x in range(0,1000)])
    return pd_arr

第一号 (创建DataFrame不会增加内存使用开销!):

Line #    Mem usage    Increment   Line Contents
================================================
    14   63.672 MiB    0.000 MiB   @profile
    15                             def to_profile():
    16  385.309 MiB  321.637 MiB       pd_arr = make_list()
    17  385.309 MiB    0.000 MiB       print sys.getrefcount(pd_arr)
    18  385.316 MiB    0.008 MiB       pd_df = pd.DataFrame(pd_arr)
    19  385.316 MiB    0.000 MiB       print sys.getsizeof(pd_arr), sys.getsizeof(pd_arr[0])
    20  386.934 MiB    1.617 MiB       print sys.getsizeof(pd_df), len(pd_arr)
    21  386.934 MiB    0.000 MiB       print sys.getrefcount(pd_arr)
    22  386.934 MiB    0.000 MiB       del pd_arr
    23  305.934 MiB  -81.000 MiB       gc.collect()

第二个数字(由于数组复制而产生的超过100Mb的开销)!:

Line #    Mem usage    Increment   Line Contents
================================================
    14   63.652 MiB    0.000 MiB   @profile
    15                             def to_profile():
    16  325.352 MiB  261.699 MiB       pd_arr = make_list()
    17  325.352 MiB    0.000 MiB       print sys.getrefcount(pd_arr)
    18  479.633 MiB  154.281 MiB       pd_df = pd.DataFrame(pd_arr)
    19  479.633 MiB    0.000 MiB       print sys.getsizeof(pd_arr), sys.getsizeof(pd_arr[0])
    20  481.602 MiB    1.969 MiB       print sys.getsizeof(pd_df), len(pd_arr)
    21  481.602 MiB    0.000 MiB       print sys.getrefcount(pd_arr)
    22  417.621 MiB  -63.980 MiB       del pd_arr
    23  330.621 MiB  -87.000 MiB       gc.collect()

所以,只使用Numpy数组而不是列表(list)来初始化DataFrame在内存消耗的角度来看更好,并且可能更快,因为它不需要额外的内存分配调用。

希望我现在已经回答了你所有的问题。


我尝试在数据框创建之前和之后计算引用计数。 代码链接 。数据框创建之前和之后引用计数为 2。 - Rajs123
我也看到了。我正在努力理解问题出在哪里。 - Vladimir Ignatev
引用计数为2是可以的。
import sys a = [1,2,3] print sys.getrefcount(a) 2
- Vladimir Ignatev
@Rajs123 请检查我的答案!在创建DataFrame时,您应该优先选择np.array而不是list,因为它更快且不需要额外的数据复制,这发生在pandas内部(证据在答案中)。 - Vladimir Ignatev
1
非常感谢!这正是我所需要的一切。另外,感谢您指出代码。现在我会经常使用它 :) - Rajs123

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