Wie gebe ich Speicher frei, der von einem Pandas-Datenrahmen verwendet wird?

111

Ich habe eine wirklich große CSV-Datei, die ich in Pandas wie folgt geöffnet habe ...

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

Sobald ich dies tue, erhöht sich meine Speichernutzung um 2 GB, was erwartet wird, da diese Datei Millionen von Zeilen enthält. Mein Problem tritt auf, wenn ich diesen Speicher freigeben muss. Ich rannte ....

del df

Meine Speichernutzung ist jedoch nicht gesunken. Ist dies der falsche Ansatz, um den von einem Pandas-Datenrahmen verwendeten Speicher freizugeben? Wenn ja, wie ist der richtige Weg?

b10hazard
quelle
3
Das ist richtig, der Garbage Collector gibt den Speicher möglicherweise nicht sofort frei. Sie können das gcModul auch importieren und aufrufen, gc.collect()aber er kann den Speicher möglicherweise nicht wiederherstellen
EdChum
del dfwird nicht direkt nach der Erstellung von df aufgerufen, oder? Ich denke, es gibt Verweise auf den df an dem Punkt, an dem Sie den df löschen. Es wird also nicht gelöscht, sondern der Name.
Marlon Abeykoon
4
Ob der vom Garbage Collector zurückgeforderte Speicher tatsächlich an das Betriebssystem zurückgegeben wird, hängt von der Implementierung ab. Die einzige Garantie, die der Garbage Collector gibt, besteht darin, dass zurückgeforderter Speicher vom aktuellen Python-Prozess für andere Zwecke verwendet werden kann, anstatt vom Betriebssystem nach mehr oder sogar mehr Speicher zu fragen .
Chepner
Ich rufe del df direkt nach der Erstellung an. Ich habe keine weiteren Verweise auf df hinzugefügt. Ich habe nur ipython geöffnet und diese drei Codezeilen ausgeführt. Wenn ich denselben Code für ein anderes Objekt ausführe, das viel Speicher benötigt, z. B. ein numpy-Array. del nparray funktioniert perfekt
b10hazard
@ b10hazard: Was ist mit so etwas wie df = ''am Ende deines Codes? Scheint den vom Datenrahmen verwendeten RAM zu löschen.
Jibounet

Antworten:

118

Das Reduzieren der Speichernutzung in Python ist schwierig, da Python den Speicher nicht an das Betriebssystem zurückgibt . Wenn Sie Objekte löschen, steht der Speicher neuen Python-Objekten zur Verfügung, jedoch nicht free()dem System ( siehe diese Frage) ).

Wenn Sie sich an numerische Numpy-Arrays halten, werden diese freigegeben, Box-Objekte jedoch nicht.

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

Reduzieren der Anzahl der Datenrahmen

Python hält unseren Speicher auf einem hohen Wasserzeichen, aber wir können die Gesamtzahl der von uns erstellten Datenrahmen reduzieren. Bevorzugen inplace=TrueSie beim Ändern Ihres Datenrahmens, damit Sie keine Kopien erstellen.

Ein weiteres häufiges Problem ist das Festhalten an Kopien zuvor in ipython erstellter Datenrahmen:

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}

Sie können dies beheben, indem Sie eingeben %reset Out, um Ihren Verlauf zu löschen. Alternativ können Sie anpassen, wie viel Verlauf ipython aufbewahrt ipython --cache-size=5(Standard ist 1000).

Reduzieren der Datenrahmengröße

Vermeiden Sie nach Möglichkeit die Verwendung von Objekt-D-Typen.

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

Werte mit einem Objekttyp werden eingerahmt. Dies bedeutet, dass das numpy-Array nur einen Zeiger enthält und Sie für jeden Wert in Ihrem Datenrahmen ein vollständiges Python-Objekt auf dem Heap haben. Dies schließt Zeichenfolgen ein.

Während numpy Zeichenfolgen mit fester Größe in Arrays unterstützt, ist dies bei Pandas nicht der Fall ( dies führt zu Verwirrung bei den Benutzern ). Dies kann einen signifikanten Unterschied machen:

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

Möglicherweise möchten Sie die Verwendung von Zeichenfolgenspalten vermeiden oder eine Möglichkeit finden, Zeichenfolgendaten als Zahlen darzustellen.

Wenn Sie einen Datenrahmen haben, der viele wiederholte Werte enthält (NaN ist sehr häufig), können Sie eine spärliche Datenstruktur verwenden , um die Speichernutzung zu reduzieren:

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

Anzeigen der Speichernutzung

Sie können die Speichernutzung ( Dokumente ) anzeigen :

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

Ab Pandas 0.17.1 können Sie auch die df.info(memory_usage='deep')Speichernutzung einschließlich der Objekte anzeigen .

Wilfred Hughes
quelle
2
Dies muss als "Akzeptierte Antwort" gekennzeichnet sein. Es erklärt kurz, aber klar, wie Python das Gedächtnis behält, auch wenn es es nicht wirklich braucht. Die Tipps zum Speichern von Speicher sind alle sinnvoll und nützlich. Als weiteren Tipp würde ich nur 'Multiprocessing' hinzufügen (wie in @ Amis Antwort erklärt).
pedram bashiri
46

Wie in den Kommentaren erwähnt, gibt es einige Dinge zu versuchen: gc.collect(@EdChum) kann zum Beispiel Dinge löschen. Zumindest aus meiner Erfahrung funktionieren diese Dinge manchmal und oft nicht.

Es gibt jedoch eine Sache, die immer funktioniert, da sie auf Betriebssystem- und nicht auf Sprachebene ausgeführt wird.

Angenommen, Sie haben eine Funktion, die einen riesigen DataFrame zwischenzeitlich erstellt und ein kleineres Ergebnis zurückgibt (das auch ein DataFrame sein kann):

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

Dann, wenn Sie so etwas tun

import multiprocessing

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

Dann wird die Funktion in einem anderen Prozess ausgeführt . Wenn dieser Vorgang abgeschlossen ist, nimmt das Betriebssystem alle verwendeten Ressourcen zurück. Es gibt wirklich nichts, was Python, Pandas, der Müllsammler, tun könnte, um das zu stoppen.

Ami Tavory
quelle
1
@ b10hazard Auch ohne Pandas habe ich nie ganz verstanden, wie Python-Speicher in der Praxis funktioniert. Diese grobe Technik ist das einzige, worauf ich mich verlasse.
Ami Tavory
9
Funktioniert wirklich gut. In einer Ipython-Umgebung (wie einem Jupyter-Notizbuch) habe ich jedoch festgestellt, dass Sie den Pool schließen, schließen und verbinden () oder beenden () müssen, um den erzeugten Prozess loszuwerden. Der einfachste Weg, dies seit Python 3.3 zu tun, ist die Verwendung des Kontextverwaltungsprotokolls: with multiprocessing.Pool(1) as pool: result = pool.map(huge_intermediate_calc, [something])Dazu muss der Pool geschlossen werden, sobald dies erledigt ist.
Zertrin
2
Dies funktioniert gut. Vergessen Sie nicht, den Pool zu beenden und ihm beizutreten, nachdem die Aufgabe erledigt ist.
Andrey Nikishaev
1
Nachdem Sie mehrmals gelesen haben, wie Sie den Speicher von einem Python-Objekt zurückfordern können, scheint dies der beste Weg zu sein, dies zu tun. Erstellen Sie einen Prozess, und wenn dieser Prozess beendet wird, gibt das Betriebssystem den Speicher frei.
Muammar
1
Vielleicht hilft es jemandem beim Erstellen des Pools, maxtasksperchild = 1 zu verwenden, um den Prozess freizugeben und nach Abschluss des Auftrags einen neuen zu erzeugen.
Giwiro
22

Dies löst das Problem, den Speicher für mich freizugeben !!!

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

Der Datenrahmen wird explizit auf null gesetzt

Hardi
quelle
1
Warum Datenrahmen in Unterliste [[df_1, df_2]] hinzugefügt? Gibt es einen bestimmten Grund? Bitte erkläre.
Goks
5
Warum verwenden Sie nicht einfach die letzten beiden Aussagen? Ich glaube nicht, dass Sie die ersten beiden Aussagen brauchen.
Spacedustpi
3

del dfwird nicht gelöscht, wenn dfzum Zeitpunkt der Löschung ein Verweis auf die vorhanden ist . Sie müssen also alle Verweise darauf löschen del df, um den Speicher freizugeben.

Daher sollten alle an df gebundenen Instanzen gelöscht werden, um die Speicherbereinigung auszulösen.

Verwenden Sie objgragh , um zu überprüfen, welche Objekte festgehalten werden.

Marlon Abeykoon
quelle
Der Link verweist auf objgraph ( mg.pov.lt/objgraph ). Es ist ein Tippfehler in Ihrer Antwort, es sei denn, es gibt ein objgragh
SatZ
1

Es scheint, dass es ein Problem mit glibc gibt, das sich auf die Speicherzuordnung in Pandas auswirkt: https://github.com/pandas-dev/pandas/issues/2659

Der in diesem Problem beschriebene Affen-Patch hat das Problem für mich behoben:

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