Como faço para liberar a memória usada por um dataframe do pandas?

111

Eu tenho um arquivo csv muito grande que abri no pandas da seguinte maneira ....

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

Depois de fazer isso, meu uso de memória aumenta em 2 GB, o que é esperado porque este arquivo contém milhões de linhas. Meu problema surge quando preciso liberar essa memória. Eu corri ....

del df

No entanto, meu uso de memória não diminuiu. É esta a abordagem errada para liberar memória usada por um quadro de dados do pandas? Se for, qual é a maneira adequada?

b10hazard
fonte
3
está correto, o coletor de lixo pode não liberar a memória imediatamente, você também pode importar o gcmódulo e chamar, gc.collect()mas ele pode não recuperar a memória
EdChum
del dfnão é chamado diretamente após a criação do df certo? Acho que há referências ao df no momento em que você exclui o df. Portanto, ele não será excluído em vez disso, ele exclui o nome.
Marlon Abeykoon
4
Se a memória recuperada pelo coletor de lixo é ou não devolvida ao sistema operacional depende da implementação; a única garantia que o coletor de lixo oferece é que a memória recuperada pode ser usada pelo processo Python atual para outras coisas, em vez de solicitar ou até mais memória do sistema operacional.
chepner
Estou ligando para del df logo após a criação. Não adicionei nenhuma outra referência ao df. Tudo o que fiz foi abrir o ipython e executar essas três linhas de código. Se eu executar o mesmo código em algum outro objeto que consome muita memória, digamos, uma matriz numpy. del nparray funciona perfeitamente
b10hazard
@ b10hazard: Que tal algo como df = ''no final do seu código? Parece limpar a RAM usada pelo dataframe.
jibounet

Respostas:

119

Reduzir o uso de memória em Python é difícil, porque Python não libera memória de volta para o sistema operacional . Se você excluir objetos, a memória estará disponível para novos objetos Python, mas não será free()devolvida ao sistema ( veja esta questão ).

Se você se limitar a matrizes numpy numéricas, elas serão liberadas, mas os objetos em caixas não.

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

Reduzindo o número de Dataframes

Python mantém nossa memória com marca d'água alta, mas podemos reduzir o número total de dataframes que criamos. Ao modificar seu dataframe, prefira inplace=True, para não criar cópias.

Outra pegadinha comum é manter cópias de dataframes criados anteriormente em 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}

Você pode corrigir isso digitando %reset Outpara limpar seu histórico. Alternativamente, você pode ajustar quanto histórico o ipython mantém ipython --cache-size=5(o padrão é 1000).

Reduzindo o tamanho do dataframe

Sempre que possível, evite usar 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

Os valores com um objeto dtype são encaixotados, o que significa que o array numpy contém apenas um ponteiro e você tem um objeto Python completo no heap para cada valor em seu dataframe. Isso inclui strings.

Enquanto o numpy suporta strings de tamanho fixo em arrays, o pandas não ( isso causa confusão ao usuário ). Isso pode fazer uma diferença 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

Você pode evitar o uso de colunas de string ou encontrar uma maneira de representar dados de string como números.

Se você tiver um dataframe que contém muitos valores repetidos (NaN é muito comum), você pode usar uma estrutura de dados esparsa para reduzir o uso de memória:

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

Visualizando o uso da memória

Você pode ver o uso de memória ( 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 do pandas 0.17.1, você também pode fazer df.info(memory_usage='deep')para ver o uso de memória, incluindo objetos.

Wilfred Hughes
fonte
2
Isso deve ser marcado como 'Resposta aceita'. Ele explica de maneira breve, mas clara, como o python mantém a memória mesmo quando realmente não precisa dela. As dicas para economizar memória são todas sensatas e úteis. Como outra dica, eu adicionaria o uso de 'multiprocessamento' (conforme explicado na resposta de
@Ami
46

Conforme observado nos comentários, existem algumas coisas para tentar: gc.collect(@EdChum) pode limpar coisas, por exemplo. Pelo menos pela minha experiência, essas coisas às vezes funcionam e muitas vezes não.

No entanto, há uma coisa que sempre funciona porque é feito no nível do sistema operacional, não no idioma.

Suponha que você tenha uma função que cria um grande DataFrame intermediário e retorna um resultado menor (que também pode ser um DataFrame):

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

Então, se você fizer algo como

import multiprocessing

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

Em seguida, a função é executada em um processo diferente . Quando esse processo é concluído, o SO retoma todos os recursos que usou. Não há realmente nada que Python, pandas, o coletor de lixo possam fazer para impedir isso.

Ami Tavory
fonte
1
@ b10hazard Mesmo sem os pandas, nunca entendi completamente como a memória Python funciona na prática. Essa técnica tosca é a única coisa em que confio.
Ami Tavory
9
Funciona muito bem. No entanto, em um ambiente ipython (como o notebook jupyter), descobri que você precisa .close () e .join () ou .terminate () o pool para se livrar do processo gerado. A maneira mais fácil de fazer isso, desde o Python 3.3, é usar o protocolo de gerenciamento de contexto: o with multiprocessing.Pool(1) as pool: result = pool.map(huge_intermediate_calc, [something])que leva a fechar o pool uma vez feito.
Zertrin,
2
Isso funciona bem, mas não se esqueça de encerrar e ingressar no pool após a conclusão da tarefa.
Andrey Nikishaev
1
Depois de ler várias vezes sobre como reivindicar de volta a memória de um objeto Python, essa parece ser a melhor maneira de fazer isso. Crie um processo e, quando esse processo for encerrado, o sistema operacional libera a memória.
muammar
1
Talvez ajude alguém, ao criar o Pool tente usar maxtasksperchild = 1 para liberar o processo e gerar um novo após o trabalho ser concluído.
giwiro
22

Isso resolve o problema de liberar a memória pra mim !!!

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

o quadro de dados será explicitamente definido como nulo

Hardi
fonte
1
Por que dataframes adicionados à sublista [[df_1, df_2]]? Algum motivo específico? Por favor explique.
goks de
5
Por que você simplesmente não usa as duas últimas afirmações? Não acho que você precise das duas primeiras declarações.
spacedustpi
3

del dfnão será excluído se houver qualquer referência a dfno momento da exclusão. Portanto, você precisa excluir todas as referências a ele del dfpara liberar a memória.

Portanto, todas as instâncias vinculadas a df devem ser excluídas para acionar a coleta de lixo.

Use objgragh para verificar qual está segurando os objetos.

Marlon Abeykoon
fonte
o link aponta para objgraph ( mg.pov.lt/objgraph ), é um erro de digitação em sua resposta, a menos que haja um objgragh
SatZ
1

Parece que há um problema com o glibc que afeta a alocação de memória no Pandas: https://github.com/pandas-dev/pandas/issues/2659

O patch monkey detalhado neste problema resolveu o problema para mim:

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