【问题标题】:Increase in memory usage on pandas dataframe creation增加 pandas 数据框创建的内存使用量
【发布时间】:2017-06-13 03:14:00
【问题描述】:

我有一段代码接收来自另一个函数的回调并创建一个列表列表 (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()

关于引入数据框,

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()

【问题讨论】:

  • 您是否完全确定代码中的其他任何地方都没有对pd_arr 的引用? Python 是引用计数的,因此使用del 只会释放关联的内存,前提是可以确保无法从任何地方使用已删除的对象。也可以试试clear the list
  • 我尝试使用del pd_arr[:]。没有减少内存。 pd_arr 在代码中被定义为全局。那会有什么不同吗?
  • 好吧,del pd_arr 只是意味着您不能再使用名称 pd_arr 来引用该列表,无论是否是全局的,但如果在以前的某个时间点有类似 a = pd_arr 的东西(尽管它可能更微妙,比如将pd_arr 传递给函数并将其引用复制到其他地方),那么它不会被真正删除。但是,我无法解释为什么 del pd_arr[:] 没有任何区别。
  • 没有其他指向 pd_arr 的引用指针。

标签: python pandas memory


【解决方案1】:

回答您的第一个问题,当您尝试使用del pd_arr 清理内存时,实际上不会发生这种情况,因为DataFrame 存储了一个指向pd_arr 的链接,而顶部范围又保留了一个链接;减少 refcounter 不会收集内存,因为该内存正在使用中。

您可以通过在del pd_arr 之前运行sys.getrefcount(pd_arr) 来检查我的假设,结果您将得到2

现在,我相信以下代码 sn-p 与您尝试执行的操作相同:https://gist.github.com/vladignatyev/ec7a26b7042efd6f710d436afbfb87de/90df8cc6bbb8bd0cb3a1d2670e03aff24f3a5b24

如果你试试这个sn-p,你会看到内存使用情况如下:

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 内存开销的位置。比较以下两个sn-ps:

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

编号 #1(创建 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()

编号 #2(由于复制数组,开销超过 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 数组初始化 DataFrame,而不是 list。从内存消耗的角度来看,它更好,而且可能更快,因为它不需要额外的内存分配调用。

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

【讨论】:

  • 我尝试在创建数据框之前和之后计算引用计数。 Code-link 。创建数据框前后的参考计数为 2。
  • 我也看到了。我正在尝试了解问题所在。
  • ref count 为 2 就可以了。 >>> import sys >>> a = [1,2,3] >>> print sys.getrefcount(a) 2
  • @Rajs123 请检查我的回答!在创建 DataFrame 期间,您必须更喜欢 np.array 而不是 list 因为它更快并且不需要额外的数据复制,这发生在pandas internals 中(答案就在答案中)。
  • 非常感谢!这就是我需要的一切。另外,感谢您指出代码。现在会经常使用它:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-02
  • 2019-06-28
  • 2020-06-05
  • 1970-01-01
相关资源
最近更新 更多