ฉันจะปล่อยหน่วยความจำที่ใช้โดยแพนด้าดาต้าเฟรมได้อย่างไร


113

ฉันมีไฟล์ csv ขนาดใหญ่มากที่ฉันเปิดในแพนด้าดังนี้ ....

import pandas
df = pandas.read_csv('large_txt_file.txt')

เมื่อฉันทำสิ่งนี้การใช้หน่วยความจำของฉันจะเพิ่มขึ้น 2GB ซึ่งคาดว่าเป็นเพราะไฟล์นี้มีแถวหลายล้านแถว ปัญหาของฉันเกิดขึ้นเมื่อฉันต้องปล่อยหน่วยความจำนี้ ฉันวิ่ง ....

del df

อย่างไรก็ตามการใช้งานหน่วยความจำของฉันไม่ลดลง นี่เป็นแนวทางที่ไม่ถูกต้องในการปล่อยหน่วยความจำที่ใช้โดยกรอบข้อมูลแพนด้าหรือไม่? ถ้าเป็นเช่นนั้นวิธีที่เหมาะสมคืออะไร?


3
ถูกต้องตัวเก็บขยะอาจไม่คลายหน่วยความจำทันทีคุณยังสามารถนำเข้าgcโมดูลและเรียกใช้gc.collect()แต่อาจไม่สามารถกู้คืนหน่วยความจำได้
EdChum

del dfไม่ได้ถูกเรียกโดยตรงหลังจากสร้าง df ใช่ไหม ฉันคิดว่ามีการอ้างอิงถึง df ณ จุดที่คุณลบ df ดังนั้นจะไม่ถูกลบ แต่จะลบชื่อ
Marlon Abeykoon

4
ไม่ว่าหน่วยความจำที่เรียกคืนโดยตัวรวบรวมขยะจะถูกส่งกลับไปยังระบบปฏิบัติการจริงหรือไม่นั้นขึ้นอยู่กับการนำไปใช้งาน สิ่งเดียวที่รับประกันได้ว่าตัวเก็บรวบรวมขยะทำคือหน่วยความจำที่เรียกคืนสามารถใช้โดยกระบวนการ Python ปัจจุบันสำหรับสิ่งอื่นแทนการขอหรือหน่วยความจำเพิ่มเติมจากระบบปฏิบัติการ
chepner

ฉันโทรหาเดล df ทันทีหลังจากสร้าง ฉันไม่ได้เพิ่มการอ้างอิงอื่น ๆ ใน df ทั้งหมดที่ฉันทำคือเปิด ipython และเรียกใช้โค้ดสามบรรทัดนั้น ถ้าฉันเรียกใช้รหัสเดียวกันกับวัตถุอื่นที่ใช้หน่วยความจำมากเช่นพูดว่าอาร์เรย์จำนวนนับ del nparray ทำงานได้อย่างสมบูรณ์
b10hazard

@ b10hazard: แล้วdf = ''ตอนท้ายของคุณรหัสล่ะ? ดูเหมือนว่าจะล้าง RAM ที่ใช้โดย dataframe
jibounet

คำตอบ:


122

การลดการใช้หน่วยความจำใน Python ทำได้ยากเนื่องจากPython ไม่ปล่อยหน่วยความจำกลับสู่ระบบปฏิบัติการจริงๆ หากคุณลบอ็อบเจ็กต์หน่วยความจำจะพร้อมใช้งานสำหรับออบเจ็กต์ Python ใหม่ แต่ไม่free()กลับไปที่ระบบ ( ดูคำถามนี้ )

หากคุณยึดติดกับอาร์เรย์ตัวเลขเหล่านั้นจะเป็นอิสระ แต่วัตถุแบบบรรจุกล่องจะไม่

>>> import os, psutil, numpy as np
>>> def usage():
...     process = psutil.Process(os.getpid())
...     return process.get_memory_info()[0] / float(2 ** 20)
... 
>>> usage() # initial memory usage
27.5 

>>> arr = np.arange(10 ** 8) # create a large array without boxing
>>> usage()
790.46875
>>> del arr
>>> usage()
27.52734375 # numpy just free()'d the array

>>> arr = np.arange(10 ** 8, dtype='O') # create lots of objects
>>> usage()
3135.109375
>>> del arr
>>> usage()
2372.16796875  # numpy frees the array, but python keeps the heap big

การลดจำนวน Dataframes

Python เก็บหน่วยความจำของเราไว้ที่ลายน้ำสูง แต่เราสามารถลดจำนวน dataframes ทั้งหมดที่เราสร้างได้ เมื่อต้องการแก้ไขดาต้าเฟรมของคุณคุณinplace=Trueจึงไม่ต้องสร้างสำเนา

gotcha ทั่วไปอีกตัวหนึ่งถือครองสำเนาของดาต้าเฟรมที่สร้างไว้ก่อนหน้านี้ใน ipython:

In [1]: import pandas as pd

In [2]: df = pd.DataFrame({'foo': [1,2,3,4]})

In [3]: df + 1
Out[3]: 
   foo
0    2
1    3
2    4
3    5

In [4]: df + 2
Out[4]: 
   foo
0    3
1    4
2    5
3    6

In [5]: Out # Still has all our temporary DataFrame objects!
Out[5]: 
{3:    foo
 0    2
 1    3
 2    4
 3    5, 4:    foo
 0    3
 1    4
 2    5
 3    6}

คุณสามารถแก้ไขได้โดยพิมพ์%reset Outเพื่อล้างประวัติของคุณ หรือคุณสามารถปรับจำนวนประวัติที่ ipython เก็บไว้ได้ipython --cache-size=5(ค่าเริ่มต้นคือ 1000)

การลดขนาด Dataframe

หากเป็นไปได้ให้หลีกเลี่ยงการใช้ประเภทวัตถุ

>>> df.dtypes
foo    float64 # 8 bytes per value
bar      int64 # 8 bytes per value
baz     object # at least 48 bytes per value, often more

ค่าที่มีอ็อบเจ็กต์ dtype อยู่ในกล่องซึ่งหมายความว่าอาร์เรย์ numpy มีเพียงตัวชี้และคุณมีวัตถุ Python แบบเต็มในฮีปสำหรับทุกค่าในดาต้าเฟรมของคุณ ซึ่งรวมถึงสตริง

ในขณะที่ numpy รองรับสตริงขนาดคงที่ในอาร์เรย์ แต่แพนด้าก็ไม่ทำ ( ทำให้ผู้ใช้สับสน ) สิ่งนี้สามารถสร้างความแตกต่างอย่างมีนัยสำคัญ:

>>> import numpy as np
>>> arr = np.array(['foo', 'bar', 'baz'])
>>> arr.dtype
dtype('S3')
>>> arr.nbytes
9

>>> import sys; import pandas as pd
>>> s = pd.Series(['foo', 'bar', 'baz'])
dtype('O')
>>> sum(sys.getsizeof(x) for x in s)
120

คุณอาจต้องการหลีกเลี่ยงการใช้คอลัมน์สตริงหรือหาวิธีแทนข้อมูลสตริงเป็นตัวเลข

หากคุณมีดาต้าเฟรมที่มีค่าซ้ำหลายค่า (NaN เป็นเรื่องปกติมาก) คุณสามารถใช้โครงสร้างข้อมูลแบบกระจัดกระจายเพื่อลดการใช้หน่วยความจำ:

>>> df1.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 39681584 entries, 0 to 39681583
Data columns (total 1 columns):
foo    float64
dtypes: float64(1)
memory usage: 605.5 MB

>>> df1.shape
(39681584, 1)

>>> df1.foo.isnull().sum() * 100. / len(df1)
20.628483479893344 # so 20% of values are NaN

>>> df1.to_sparse().info()
<class 'pandas.sparse.frame.SparseDataFrame'>
Int64Index: 39681584 entries, 0 to 39681583
Data columns (total 1 columns):
foo    float64
dtypes: float64(1)
memory usage: 543.0 MB

การดูการใช้งานหน่วยความจำ

คุณสามารถดูการใช้งานหน่วยความจำ ( เอกสาร ):

>>> df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 39681584 entries, 0 to 39681583
Data columns (total 14 columns):
...
dtypes: datetime64[ns](1), float64(8), int64(1), object(4)
memory usage: 4.4+ GB

สำหรับแพนด้า 0.17.1 คุณสามารถdf.info(memory_usage='deep')ดูการใช้หน่วยความจำรวมถึงวัตถุ


2
จะต้องมีการทำเครื่องหมายว่า "คำตอบที่ยอมรับ" มันอธิบายสั้น ๆ แต่ชัดเจนว่า python มีความทรงจำอย่างไรแม้ว่าจะไม่ต้องการจริงๆก็ตาม เคล็ดลับในการประหยัดหน่วยความจำล้วนมีเหตุผลและมีประโยชน์ เป็นเคล็ดลับอื่นฉันจะเพิ่มโดยใช้ 'การประมวลผลหลายขั้นตอน' (ตามที่อธิบายไว้ในคำตอบของ @ Ami
pedram bashiri

46

ตามที่ระบุไว้ในความคิดเห็นมีบางสิ่งที่ควรลอง: gc.collect(@EdChum) อาจล้างสิ่งต่างๆเช่น อย่างน้อยจากประสบการณ์ของฉันสิ่งเหล่านี้บางครั้งก็ใช้ได้ผลและมักไม่ได้ผล

อย่างไรก็ตามมีสิ่งหนึ่งที่ใช้งานได้เสมอเพราะทำที่ OS ไม่ใช่ภาษาระดับ

สมมติว่าคุณมีฟังก์ชันที่สร้าง DataFrame ขนาดใหญ่ระดับกลางและส่งกลับผลลัพธ์ที่เล็กกว่า (ซึ่งอาจเป็น DataFrame ด้วย):

def huge_intermediate_calc(something):
    ...
    huge_df = pd.DataFrame(...)
    ...
    return some_aggregate

แล้วถ้าคุณทำสิ่งที่ชอบ

import multiprocessing

result = multiprocessing.Pool(1).map(huge_intermediate_calc, [something_])[0]

จากนั้นฟังก์ชันจะถูกเรียกใช้ในกระบวนการอื่น เมื่อกระบวนการนั้นเสร็จสิ้น OS จะดึงทรัพยากรทั้งหมดที่ใช้ไปกลับคืนมา ไม่มีอะไรที่ Python แพนด้านักเก็บขยะสามารถทำได้เพื่อหยุดสิ่งนั้น


1
@ b10hazard แม้จะไม่มีแพนด้า แต่ฉันไม่เคยเข้าใจเลยว่าหน่วยความจำ Python ทำงานอย่างไรในทางปฏิบัติ เทคนิคหยาบนี้เป็นสิ่งเดียวที่ฉันพึ่งพา
Ami Tavory

9
ใช้งานได้ดีจริงๆ อย่างไรก็ตามในสภาพแวดล้อม ipython (เช่นสมุดบันทึก jupyter) ฉันพบว่าคุณต้อง .close () และ .join () หรือ .terminate () พูลเพื่อกำจัดกระบวนการสร้าง วิธีที่ง่ายที่สุดในการทำเช่นนั้นเนื่องจาก Python 3.3 คือการใช้โปรโตคอลการจัดการบริบทwith multiprocessing.Pool(1) as pool: result = pool.map(huge_intermediate_calc, [something])ซึ่งใช้เวลาในการปิดพูลเมื่อเสร็จสิ้น
Zertrin

2
วิธีนี้ใช้งานได้ดีอย่าลืมยุติและเข้าร่วมพูลหลังจากทำงานเสร็จแล้ว
Andrey Nikishaev

1
หลังจากอ่านหลายครั้งเกี่ยวกับวิธีการเรียกคืนหน่วยความจำจากวัตถุ python ดูเหมือนว่าจะเป็นวิธีที่ดีที่สุดในการทำเช่นนั้น สร้างกระบวนการและเมื่อกระบวนการนั้นถูกฆ่าระบบปฏิบัติการจะปล่อยหน่วยความจำ
muammar

1
บางทีอาจช่วยใครบางคนได้เมื่อสร้าง Pool พยายามใช้ maxtasksperchild = 1 เพื่อปล่อยกระบวนการและวางไข่ใหม่หลังจากเสร็จงาน
giwiro

23

วิธีนี้แก้ปัญหาการปล่อยหน่วยความจำให้ฉันได้ !!!

del [[df_1,df_2]]
gc.collect()
df_1=pd.DataFrame()
df_2=pd.DataFrame()

data-frame จะถูกตั้งค่าเป็น null อย่างชัดเจน


1
เหตุใดจึงเพิ่มเฟรมข้อมูลในรายการย่อย [[df_1, df_2]] มีเหตุผลเฉพาะหรือไม่? กรุณาอธิบาย.
goks

5
ทำไมคุณไม่ใช้สองประโยคสุดท้าย ฉันไม่คิดว่าคุณต้องการสองงบแรก
spacedustpi

3

del dfจะไม่ถูกลบหากมีการอ้างอิงถึงdfเวลาที่ลบ ดังนั้นคุณต้องลบการอ้างอิงทั้งหมดdel dfเพื่อปล่อยหน่วยความจำ

ดังนั้นควรลบอินสแตนซ์ทั้งหมดที่เชื่อมโยงกับ df เพื่อทริกเกอร์การรวบรวมขยะ

ใช้objgraghเพื่อตรวจสอบว่าวัตถุใดถืออยู่


ลิงก์ชี้ไปที่ objgraph ( mg.pov.lt/objgraph ) เป็นการพิมพ์คำตอบของคุณผิดเว้นแต่จะมี objgragh
SatZ

1

ดูเหมือนว่ามีปัญหาเกี่ยวกับ glibc ที่ส่งผลต่อการจัดสรรหน่วยความจำใน Pandas: https://github.com/pandas-dev/pandas/issues/2659

แพทช์ลิงรายละเอียดเกี่ยวกับเรื่องนี้ได้รับการแก้ไขปัญหาที่เกิดขึ้นสำหรับฉัน:

# monkeypatches.py

# Solving memory leak problem in pandas
# https://github.com/pandas-dev/pandas/issues/2659#issuecomment-12021083
import pandas as pd
from ctypes import cdll, CDLL
try:
    cdll.LoadLibrary("libc.so.6")
    libc = CDLL("libc.so.6")
    libc.malloc_trim(0)
except (OSError, AttributeError):
    libc = None

__old_del = getattr(pd.DataFrame, '__del__', None)

def __new_del(self):
    if __old_del:
        __old_del(self)
    libc.malloc_trim(0)

if libc:
    print('Applying monkeypatch for pd.DataFrame.__del__', file=sys.stderr)
    pd.DataFrame.__del__ = __new_del
else:
    print('Skipping monkeypatch for pd.DataFrame.__del__: libc or malloc_trim() not found', file=sys.stderr)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.