¿Cómo libero la memoria utilizada por un marco de datos de pandas?

111

Tengo un archivo csv realmente grande que abrí en pandas de la siguiente manera ...

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

Una vez que hago esto, mi uso de memoria aumenta en 2 GB, lo que se espera porque este archivo contiene millones de filas. Mi problema viene cuando necesito liberar esta memoria. Corrí ...

del df

Sin embargo, mi uso de memoria no disminuyó. ¿Es este el enfoque incorrecto para liberar la memoria utilizada por un marco de datos de pandas? Si es así, ¿cuál es la forma correcta?

b10hazard
fuente
3
eso es correcto, es posible que el recolector de basura no libere la memoria de inmediato, también puede importar el gcmódulo y llamar, gc.collect()pero es posible que no recupere la memoria
EdChum
del dfno se llama directamente después de la creación de df, ¿verdad? Creo que hay referencias al df en el momento en que elimina el df. Por lo tanto, no se eliminará, sino que eliminará el nombre.
Marlon Abeykoon
4
Si la memoria reclamada por el recolector de basura se devuelve o no al sistema operativo depende de la implementación; la única garantía que hace el recolector de basura es que el proceso actual de Python puede usar la memoria recuperada para otras cosas en lugar de pedir o incluso más memoria del sistema operativo.
chepner
Estoy llamando del df justo después de la creación. No agregué ninguna otra referencia a df. Todo lo que hice fue abrir ipython y ejecutar esas tres líneas de código. Si ejecuto el mismo código en algún otro objeto que ocupa mucha memoria, como por ejemplo una matriz numpy. del nparray funciona perfectamente
b10hazard
@ b10hazard: ¿Qué tal algo como df = ''al final de su código? Parece borrar la RAM utilizada por el marco de datos.
jibounet

Respuestas:

119

Reducir el uso de memoria en Python es difícil, porque Python en realidad no libera memoria al sistema operativo . Si elimina objetos, la memoria estará disponible para nuevos objetos de Python, pero no free()volverá al sistema ( consulte esta pregunta ).

Si se adhiere a matrices numéricas numéricas, esas se liberan, pero los objetos en caja no.

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

Reducir el número de marcos de datos

Python mantiene nuestra memoria en una marca de agua alta, pero podemos reducir la cantidad total de marcos de datos que creamos. Al modificar su marco de datos, prefiera inplace=True, para que no cree copias.

Otro problema común es aferrarse a copias de marcos de datos creados previamente en 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}

Puede solucionar este problema escribiendo %reset Outpara borrar su historial. Alternativamente, puede ajustar la cantidad de historial que guarda ipython ipython --cache-size=5(el valor predeterminado es 1000).

Reducir el tamaño del marco de datos

Siempre que sea posible, evite el uso de tipos de objetos.

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

Los valores con un tipo de objeto están encuadrados, lo que significa que la matriz numpy solo contiene un puntero y tienes un objeto Python completo en el montón para cada valor en tu marco de datos. Esto incluye cadenas.

Si bien numpy admite cadenas de tamaño fijo en matrices, pandas no lo hace ( ha causado confusión en el usuario ). Esto puede marcar una diferencia significativa:

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

Es posible que desee evitar el uso de columnas de cadena o encontrar una forma de representar los datos de cadena como números.

Si tiene un marco de datos que contiene muchos valores repetidos (NaN es muy común), entonces puede usar una estructura de datos dispersa para reducir el uso de memoria:

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

Visualización del uso de memoria

Puede ver el uso de memoria ( documentos ):

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

A partir de pandas 0.17.1, también puede hacer df.info(memory_usage='deep')para ver el uso de la memoria, incluidos los objetos.

Wilfred Hughes
fuente
2
Esto debe estar marcado como 'Respuesta aceptada'. Explica breve pero claramente cómo Python se aferra a la memoria incluso cuando realmente no la necesita. Los consejos para ahorrar memoria son todos prácticos y útiles. Como otro consejo, simplemente agregaría el uso de 'multiprocesamiento' (como se explica en la respuesta de @ Ami.
pedram bashiri
46

Como se señaló en los comentarios, hay algunas cosas que puede probar: gc.collect(@EdChum) puede borrar cosas, por ejemplo. Al menos desde mi experiencia, estas cosas a veces funcionan y a menudo no.

Sin embargo, hay una cosa que siempre funciona porque se hace en el sistema operativo, no en el idioma.

Supongamos que tiene una función que crea un DataFrame enorme intermedio y devuelve un resultado más pequeño (que también podría ser un DataFrame):

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

Entonces si haces algo como

import multiprocessing

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

Luego, la función se ejecuta en un proceso diferente . Cuando se completa ese proceso, el sistema operativo retoma todos los recursos que utilizó. Realmente no hay nada que Python, pandas, el recolector de basura, puedan hacer para detener eso.

Ami Tavory
fuente
1
@ b10hazard Incluso sin pandas, nunca he entendido completamente cómo funciona la memoria de Python en la práctica. Esta tosca técnica es lo único en lo que confío.
Ami Tavory
9
Funciona muy bien. Sin embargo, en un entorno ipython (como jupyter notebook) encontré que necesita .close () y .join () o .terminate () el grupo para deshacerse del proceso generado. La forma más fácil de hacerlo desde Python 3.3 es utilizar el protocolo de gestión de contexto: lo with multiprocessing.Pool(1) as pool: result = pool.map(huge_intermediate_calc, [something])que implica cerrar el grupo una vez hecho.
Zertrin
2
Esto funciona bien, pero no olvide terminar y unirse al grupo una vez finalizada la tarea.
Andrey Nikishaev
1
Después de leer varias veces sobre cómo recuperar la memoria de un objeto de Python, esta parece ser la mejor manera de hacerlo. Cree un proceso, y cuando ese proceso se mata, el sistema operativo libera la memoria.
muammar
1
Tal vez ayude a alguien, al crear el grupo, intente usar maxtasksperchild = 1 para liberar el proceso y generar uno nuevo después de que se complete el trabajo.
giwiro
22

¡Esto resuelve el problema de liberarme la memoria!

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

el marco de datos se establecerá explícitamente en nulo

hardi
fuente
1
¿Por qué se agregaron marcos de datos en la sublista [[df_1, df_2]]? ¿Alguna razón específica? Por favor explique.
goks
5
¿Por qué no utiliza las dos últimas declaraciones? No creo que necesites las dos primeras declaraciones.
spacedustpi
3

del dfno se eliminará si hay alguna referencia al dfen el momento de la eliminación. Por lo tanto, debe eliminar todas las referencias del dfpara liberar la memoria.

Por lo tanto, todas las instancias vinculadas a df deben eliminarse para activar la recolección de basura.

Utilice objgragh para comprobar cuál está sujetando los objetos.

Marlon Abeykoon
fuente
el enlace apunta a objgraph ( mg.pov.lt/objgraph ), es un error tipográfico en su respuesta a menos que haya un objgragh
SatZ
1

Parece que hay un problema con glibc que afecta la asignación de memoria en Pandas: https://github.com/pandas-dev/pandas/issues/2659

El parche de mono detallado sobre este problema me ha resuelto el problema:

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