Comment libérer de la mémoire utilisée par une trame de données pandas?

111

J'ai un très gros fichier csv que j'ai ouvert dans les pandas comme suit ...

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

Une fois que je fais cela, mon utilisation de la mémoire augmente de 2 Go, ce qui est attendu car ce fichier contient des millions de lignes. Mon problème survient lorsque j'ai besoin de libérer cette mémoire. L'Iran....

del df

Cependant, mon utilisation de la mémoire n'a pas baissé. Est-ce la mauvaise approche pour libérer la mémoire utilisée par une trame de données pandas? Si c'est le cas, quelle est la bonne manière?

b10hazard
la source
3
c'est correct, le ramasse-miettes peut ne pas libérer la mémoire tout de suite, vous pouvez également importer le gcmodule et appeler gc.collect()mais il peut ne pas récupérer la mémoire
EdChum
del dfn'est pas appelé directement après la création de df droit? Je pense qu'il y a des références au df au moment où vous supprimez le df. Donc, il ne sera pas supprimé au lieu de cela, il supprime le nom.
Marlon Abeykoon
4
Le fait que la mémoire récupérée par le garbage collector soit réellement restituée au système d'exploitation dépend de l'implémentation; la seule garantie du garbage collector est que la mémoire récupérée peut être utilisée par le processus Python actuel pour d'autres choses au lieu de demander ou même plus de mémoire au système d'exploitation.
chepner
J'appelle del df juste après la création. Je n'ai pas ajouté d'autres références à df. Tout ce que j'ai fait, c'est d'ouvrir ipython et d'exécuter ces trois lignes de code. Si j'exécute le même code sur un autre objet qui prend beaucoup de mémoire, comme par exemple un tableau numpy. del nparray fonctionne parfaitement
b10hazard
@ b10hazard: Et quelque chose comme df = ''à la fin de votre code? Semble effacer la RAM utilisée par la trame de données.
jibounet du

Réponses:

120

Réduire l'utilisation de la mémoire en Python est difficile, car Python ne libère pas réellement de mémoire vers le système d'exploitation . Si vous supprimez des objets, alors la mémoire est disponible pour les nouveaux objets Python, mais pas free()pour revenir au système ( voir cette question ).

Si vous vous en tenez aux tableaux numpy numériques, ceux-ci sont libérés, mais les objets encadrés ne le sont pas.

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

Réduction du nombre de Dataframes

Python garde notre mémoire à un filigrane élevé, mais nous pouvons réduire le nombre total de dataframes que nous créons. Lors de la modification de votre dataframe, préférez inplace=True, afin de ne pas créer de copies.

Un autre piège courant consiste à conserver des copies de dataframes précédemment créées dans 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}

Vous pouvez résoudre ce problème en tapant %reset Out pour effacer votre historique. Vous pouvez également ajuster la quantité d'historique conservée par ipythonipython --cache-size=5 (la valeur par défaut est 1000).

Réduction de la taille de Dataframe

Dans la mesure du possible, évitez d'utiliser des types d'objets.

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

Les valeurs avec un objet dtype sont encadrées, ce qui signifie que le tableau numpy contient juste un pointeur et que vous avez un objet Python complet sur le tas pour chaque valeur de votre dataframe. Cela inclut les chaînes.

Alors que numpy prend en charge les chaînes de taille fixe dans les tableaux, les pandas ne le font pas ( cela a causé une confusion chez l'utilisateur ). Cela peut faire une différence significative:

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

Vous souhaiterez peut-être éviter d'utiliser des colonnes de chaîne ou trouver un moyen de représenter les données de chaîne sous forme de nombres.

Si vous avez une trame de données qui contient de nombreuses valeurs répétées (NaN est très courant), vous pouvez utiliser une structure de données éparse pour réduire l'utilisation de la mémoire:

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

Affichage de l'utilisation de la mémoire

Vous pouvez afficher l'utilisation de la mémoire ( docs ):

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

Depuis pandas 0.17.1, vous pouvez également df.info(memory_usage='deep')voir l'utilisation de la mémoire, y compris les objets.

Wilfred Hughes
la source
2
Cela doit être marqué «Réponse acceptée». Il explique brièvement mais clairement comment python conserve la mémoire même s'il n'en a pas vraiment besoin. Les conseils pour économiser de la mémoire sont tous judicieux et utiles. Comme autre astuce, je voudrais simplement ajouter l'utilisation du `` multiprocessing '' (comme expliqué dans la réponse de @ Ami.
pedram bashiri
46

Comme indiqué dans les commentaires, il y a certaines choses à essayer: gc.collect(@EdChum) peut effacer des choses, par exemple. Au moins d'après mon expérience, ces choses fonctionnent parfois et souvent pas.

Cependant, il y a une chose qui fonctionne toujours, car elle est effectuée au niveau du système d'exploitation, et non au niveau de la langue.

Supposons que vous ayez une fonction qui crée un énorme DataFrame intermédiaire et renvoie un résultat plus petit (qui peut également être un DataFrame):

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

Ensuite, si vous faites quelque chose comme

import multiprocessing

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

Ensuite, la fonction est exécutée à un processus différent . Une fois ce processus terminé, le système d'exploitation reprend toutes les ressources qu'il a utilisées. Il n'y a vraiment rien que Python, pandas, le ramasse-miettes, puisse faire pour arrêter cela.

Ami Tavory
la source
1
@ b10hazard Même sans pandas, je n'ai jamais complètement compris comment la mémoire Python fonctionne dans la pratique. Cette technique grossière est la seule chose sur laquelle je me fie.
Ami Tavory
9
Fonctionne vraiment bien. Cependant, dans un environnement ipython (comme jupyter notebook), j'ai trouvé que vous devez .close () et .join () ou .terminate () le pool pour vous débarrasser du processus engendré. Le moyen le plus simple de le faire depuis Python 3.3 est d'utiliser le protocole de gestion de contexte: il with multiprocessing.Pool(1) as pool: result = pool.map(huge_intermediate_calc, [something])faut fermer le pool une fois terminé.
Zertrin
2
Cela fonctionne bien, n'oubliez pas de terminer et de rejoindre le pool une fois la tâche terminée.
Andrey Nikishaev
1
Après avoir lu plusieurs fois sur la façon de récupérer la mémoire d'un objet python, cela semble être le meilleur moyen de le faire. Créez un processus, et lorsque ce processus est tué, le système d'exploitation libère la mémoire.
muammar
1
Peut-être que cela aide quelqu'un, lors de la création du pool, essayez d'utiliser maxtasksperchild = 1 afin de libérer le processus et d'en créer un nouveau une fois le travail terminé.
giwiro
22

Cela résout le problème de la libération de la mémoire pour moi !!!

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

la trame de données sera explicitement définie sur null

hardi
la source
1
Pourquoi les dataframes ajoutés dans la sous-liste [[df_1, df_2]]? Une raison spécifique? S'il vous plaît, expliquez.
goks
5
Pourquoi n'utilisez-vous pas simplement les deux dernières déclarations? Je ne pense pas que vous ayez besoin des deux premières déclarations.
spacedustpi
3

del dfne sera pas supprimé s'il y a une référence au dfau moment de la suppression. Vous devez donc supprimer toutes les références à celui-ci avec del dfpour libérer la mémoire.

Ainsi, toutes les instances liées à df doivent être supprimées pour déclencher le garbage collection.

Utilisez objgragh pour vérifier ce qui retient les objets.

Marlon Abeykoon
la source
le lien pointe vers objgraph ( mg.pov.lt/objgraph ), c'est une faute de frappe dans votre réponse à moins qu'il y ait un objgragh
SatZ
1

Il semble qu'il y ait un problème avec la glibc qui affecte l'allocation de mémoire dans Pandas: https://github.com/pandas-dev/pandas/issues/2659

Le patch de singe détaillé sur ce problème a résolu le problème pour moi:

# 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
la source