Jak zwolnić pamięć używaną przez ramkę danych pandy?

111

Mam naprawdę duży plik csv, który otworzyłem w pandach w następujący sposób ....

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

Gdy to zrobię, moje użycie pamięci wzrośnie o 2 GB, co jest oczekiwane, ponieważ ten plik zawiera miliony wierszy. Mój problem pojawia się, gdy muszę uwolnić tę pamięć. Pobiegłem ...

del df

Jednak moje użycie pamięci nie spadło. Czy to niewłaściwe podejście do zwolnienia pamięci używanej przez ramkę danych pandy? Jeśli tak, jaki jest właściwy sposób?

b10hazard
źródło
3
że jest poprawne, garbage collector nie może zwolnić pamięć od razu, można również zaimportować gcmoduł i zadzwonić gc.collect(), ale nie może odzyskać pamięć
EdChum
del dfnie jest wywoływana bezpośrednio po utworzeniu df, prawda? Myślę, że w miejscu usunięcia pliku df są odniesienia do pliku df. Więc nie zostanie usunięty, zamiast tego usuwa nazwę.
Marlon Abeykoon
4
To, czy pamięć odzyskana przez moduł odśmiecania pamięci jest faktycznie zwracana do systemu operacyjnego, zależy od implementacji; jedyną gwarancją, jaką daje moduł odśmiecający, jest to, że odzyskana pamięć może być używana przez bieżący proces Pythona do innych celów, zamiast prosić system operacyjny lub nawet o więcej pamięci.
chepner
Dzwonię do del df zaraz po stworzeniu. Nie dodałem żadnych innych odniesień do df. Wszystko, co zrobiłem, to otworzyłem ipython i uruchomiłem te trzy linie kodu. Jeśli uruchomię ten sam kod na innym obiekcie, który zajmuje dużo pamięci, jak powiedzmy tablica numpy. del nparray działa idealnie
b10hazard
@ b10hazard: A co z czymś takim jak df = ''na końcu twojego kodu? Wygląda na to, że ramka danych wyczyściła pamięć RAM.
jibounet

Odpowiedzi:

120

Zmniejszenie zużycia pamięci w Pythonie jest trudne, ponieważ Python w rzeczywistości nie zwalnia pamięci z powrotem do systemu operacyjnego . Jeśli usuniesz obiekty, pamięć będzie dostępna dla nowych obiektów Pythona, ale nie free()wróci do systemu ( zobacz to pytanie ).

Jeśli trzymasz się numerycznych tablic numpy, są one uwalniane, ale obiekty w pudełkach nie.

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

Zmniejszanie liczby ramek danych

Python utrzymuje naszą pamięć na wysokim znaku wodnym, ale możemy zmniejszyć całkowitą liczbę tworzonych ramek danych. Podczas modyfikowania ramki danych preferuj inplace=True, aby nie tworzyć kopii.

Innym częstym problemem jest zatrzymywanie kopii wcześniej utworzonych ramek danych w ipythonie:

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}

Możesz to naprawić, wpisując, %reset Outaby wyczyścić historię. Alternatywnie możesz dostosować, ile historii przechowuje ipython ipython --cache-size=5(domyślnie jest to 1000).

Zmniejszanie rozmiaru ramki danych

Jeśli to możliwe, unikaj używania dtypów obiektów.

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

Wartości z typem obiektu są opakowane w ramkę, co oznacza, że ​​tablica numpy zawiera po prostu wskaźnik i masz pełny obiekt Pythona na stercie dla każdej wartości w ramce danych. Obejmuje to ciągi.

Podczas gdy numpy obsługuje ciągi o stałym rozmiarze w tablicach, pandy nie ( powoduje to zamieszanie użytkownika ). Może to mieć istotne znaczenie:

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

Możesz uniknąć używania kolumn łańcuchowych lub znaleźć sposób na przedstawienie danych w postaci ciągów jako liczb.

Jeśli masz ramkę danych, która zawiera wiele powtarzających się wartości (NaN jest bardzo powszechne), możesz użyć rzadkiej struktury danych, aby zmniejszyć zużycie pamięci:

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

Przeglądanie wykorzystania pamięci

Możesz wyświetlić użycie pamięci ( dokumenty ):

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

Począwszy od pandy 0.17.1, możesz również df.info(memory_usage='deep')zobaczyć użycie pamięci, w tym obiekty.

Wilfred Hughes
źródło
2
To musi być oznaczone jako „Odpowiedź zaakceptowana”. Krótko, ale jasno wyjaśnia, w jaki sposób Python zachowuje pamięć, nawet jeśli tak naprawdę jej nie potrzebuje. Wszystkie wskazówki dotyczące oszczędzania pamięci są rozsądne i przydatne. Jako kolejną wskazówkę dodałbym po prostu użycie „przetwarzania wieloprocesowego” (jak wyjaśniono w odpowiedzi @ Ami.
pedram bashiri
46

Jak zauważono w komentarzach, jest kilka rzeczy do wypróbowania: gc.collect(@EdChum) może na przykład wyczyścić rzeczy. Przynajmniej z mojego doświadczenia, te rzeczy czasami działają, a często nie.

Jest jednak jedna rzecz, która zawsze działa, ponieważ jest wykonywana na poziomie systemu operacyjnego, a nie języka.

Załóżmy, że masz funkcję, która tworzy pośrednią ogromną ramkę DataFrame i zwraca mniejszy wynik (którym może być również DataFrame):

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

Wtedy jeśli zrobisz coś takiego

import multiprocessing

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

Następnie funkcja jest wykonywana w innym procesie . Po zakończeniu tego procesu system operacyjny ponownie pobiera wszystkie wykorzystane zasoby. Naprawdę nie ma nic, co mógłby zrobić Python, pandy i śmieciarki, aby to powstrzymać.

Ami Tavory
źródło
1
@ b10hazard Nawet bez pand nigdy w pełni nie zrozumiałem, jak w praktyce działa pamięć Pythona. Opieram się tylko na tej prymitywnej technice.
Ami Tavory
9
Działa naprawdę dobrze. Jednak w środowisku ipython (takim jak jupyter notebook) odkryłem, że musisz .close () i .join () lub .terminate () puli, aby pozbyć się spawnowanego procesu. Najłatwiejszym sposobem zrobienia tego od czasu Pythona 3.3 jest użycie protokołu zarządzania kontekstem: with multiprocessing.Pool(1) as pool: result = pool.map(huge_intermediate_calc, [something])co oznacza zamknięcie puli po zakończeniu.
Zertrin
2
To działa dobrze, po prostu nie zapomnij zakończyć i dołączyć do puli po zakończeniu zadania.
Andrey Nikishaev
1
Po kilkukrotnym przeczytaniu, jak odzyskać pamięć z obiektu Pythona, wydaje się, że jest to najlepszy sposób na zrobienie tego. Utwórz proces, a kiedy ten proces zostanie zabity, system operacyjny zwolni pamięć.
muammar
1
Może to komuś pomoże, podczas tworzenia puli spróbuj użyć maxtasksperchild = 1, aby zwolnić proces i odrodzić nowy po zakończeniu pracy.
giwiro
22

To rozwiązuje problem zwolnienia pamięci dla mnie !!!

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

ramka danych zostanie jawnie ustawiona na wartość null

hardi
źródło
1
Dlaczego ramki danych zostały dodane na liście podrzędnej [[df_1, df_2]]? Z jakiegoś konkretnego powodu? Proszę wytłumacz.
goks
5
Dlaczego nie użyjesz po prostu dwóch ostatnich instrukcji? Myślę, że nie potrzebujesz pierwszych dwóch stwierdzeń.
spacedustpi
3

del dfnie zostanie usunięty, jeśli istnieją jakiekolwiek odniesienia do dfczasu usunięcia. Musisz więc usunąć wszystkie odniesienia do niego za pomocą, del dfaby zwolnić pamięć.

Dlatego wszystkie wystąpienia powiązane z df powinny zostać usunięte, aby wyzwolić czyszczenie pamięci.

Użyj objgragh, aby sprawdzić, który trzyma obiekty.

Marlon Abeykoon
źródło
odsyłacz wskazuje na objgraph ( mg.pov.lt/objgraph ), jest to literówka w Twojej odpowiedzi, chyba że jest to objgragh
SatZ
1

Wygląda na to, że istnieje problem z glibc, który wpływa na alokację pamięci w Pandach: https://github.com/pandas-dev/pandas/issues/2659

Łata małpa wyszczególnione w tej sprawie został rozwiązany problem dla mnie:

# 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)
MarkNS
źródło