Ao usar a biblioteca do pandas para analisar pequenos conjuntos de dados, cujo tamanho não excede 100 megabytes, o desempenho raramente se torna um problema. Mas quando se trata do estudo de conjuntos de dados, cujos tamanhos podem atingir vários gigabytes, problemas de desempenho podem levar a um aumento significativo na duração da análise de dados e até a incapacidade de realizar análises devido à falta de memória.
Embora ferramentas como o Spark possam processar com eficiência grandes conjuntos de dados (de centenas de gigabytes a vários terabytes), para utilizar totalmente seus recursos, você geralmente precisa de um hardware bastante poderoso e caro. E, em comparação com os pandas, eles não diferem em ricos conjuntos de ferramentas para limpeza, pesquisa e análise de dados de alta qualidade. Para conjuntos de dados de tamanho médio, é melhor tentar usar pandas com mais eficiência, em vez de mudar para outras ferramentas.

No material, cuja tradução publicamos hoje, falaremos sobre as peculiaridades de trabalhar com memória ao usar pandas e como reduzir o consumo de memória em quase 90%, selecionando os tipos de dados apropriados armazenados nas colunas das estruturas de dados da tabela do
DataFrame
.
Trabalhando com dados em jogos de beisebol
Trabalharemos com dados dos jogos de beisebol da Major League coletados ao longo de 130 anos e retirados do
Retrosheet .
Inicialmente, esses dados foram apresentados como 127 arquivos CSV, mas os combinamos em um conjunto de dados usando o
csvkit e adicionamos, como a primeira linha da tabela resultante, uma linha com nomes de colunas. Se desejar, você pode fazer
o download da
nossa versão desses dados e experimentá-los, lendo o artigo.
Vamos começar importando um conjunto de dados e dando uma olhada nas cinco primeiras linhas. Você pode encontrá-los
nesta tabela, no
folha do
.
import pandas as pd gl = pd.read_csv('game_logs.csv') gl.head()
Abaixo estão as informações sobre as colunas mais importantes da tabela com esses dados. Se você quiser ler as explicações para todas as colunas,
aqui pode encontrar um dicionário de dados para todo o conjunto de dados.
date
- data do jogo.v_name
- O nome da equipe convidada.v_league
- Liga da equipe visitante.h_name
- O nome do time da casa.h_league
- A liga dos times da casa.v_score
- Pontos da equipe visitante.h_score
- Pontos da equipe da casa.v_line_score
- Um resumo dos pontos da equipe de convidados, por exemplo - 010000(10)00
.h_line_score
- Um resumo dos pontos da equipe da casa, por exemplo - 010000(10)0X
.park_id
- O identificador do campo em que o jogo foi jogado.attendance
- O número de espectadores.
Para descobrir informações gerais sobre o objeto
DataFrame
, você pode usar o método
DataFrame.info () . Graças a esse método, você pode aprender sobre o tamanho de um objeto, sobre tipos de dados e sobre o uso de memória.
Por padrão, os pandas, por uma questão de economia de tempo,
DataFrame
informações aproximadas sobre o uso de memória pelo
DataFrame
. Como estamos interessados em informações precisas, definiremos o parâmetro
memory_usage
como
'deep'
.
gl.info(memory_usage='deep')
Aqui estão as informações que conseguimos obter:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 171907 entries, 0 to 171906 Columns: 161 entries, date to acquisition_info dtypes: float64(77), int64(6), object(78) memory usage: 861.6 MB
Como se viu, temos 171.907 linhas e 161 colunas. A biblioteca do pandas detectou automaticamente os tipos de dados. Existem 83 colunas com dados numéricos e 78 colunas com objetos. As colunas de objeto são usadas para armazenar dados de sequência e nos casos em que a coluna contém dados de tipos diferentes.
Agora, para entender melhor como você pode otimizar o uso da memória com este
DataFrame
, vamos falar sobre como o pandas armazena dados na memória.
Visualização interna de um DataFrame
Dentro dos pandas, as colunas de dados são agrupadas em blocos com valores do mesmo tipo. Aqui está um exemplo de como as 12 primeiras colunas de um
DataFrame
são armazenadas em pandas.
Representação interna de diferentes tipos de dados em pandasVocê pode perceber que os blocos não armazenam informações sobre o nome da coluna. Isso ocorre porque os blocos são otimizados para armazenar os valores disponíveis nas células da tabela do objeto
DataFrame
. A classe
BlockManager
é responsável por armazenar informações sobre a correspondência entre os índices de linha e coluna do conjunto de dados e o que é armazenado em blocos do mesmo tipo de dados. Ele desempenha o papel de uma API que fornece acesso a dados básicos. Quando lemos, editamos ou
DataFrame
valores, a classe
DataFrame
interage com a classe
BlockManager
para converter nossos pedidos em chamadas de função e método.
Cada tipo de dados possui uma classe especializada no módulo
pandas.core.internals
. Por exemplo, o pandas usa a classe
ObjectBlock
para representar blocos que contêm colunas de string e a classe
FloatBlock
para representar blocos que contêm colunas que
FloatBlock
números de ponto flutuante. Para blocos que representam valores numéricos que se parecem com números inteiros ou números de ponto flutuante, o pandas combina as colunas e as armazena como a
ndarray
dados
ndarray
biblioteca NumPy. Essa estrutura de dados é baseada na matriz C, os valores são armazenados em um bloco contínuo de memória. Graças a esse esquema de armazenamento de dados, o acesso aos fragmentos de dados é muito rápido.
Como dados de tipos diferentes são armazenados separadamente, examinamos o uso de memória de diferentes tipos de dados. Vamos começar com o uso médio de memória para diferentes tipos de dados.
for dtype in ['float','int','object']: selected_dtype = gl.select_dtypes(include=[dtype]) mean_usage_b = selected_dtype.memory_usage(deep=True).mean() mean_usage_mb = mean_usage_b / 1024 ** 2 print("Average memory usage for {} columns: {:03.2f} MB".format(dtype,mean_usage_mb))
Como resultado, verifica-se que os indicadores médios de uso de memória para dados de diferentes tipos são assim:
Average memory usage for float columns: 1.29 MB Average memory usage for int columns: 1.12 MB Average memory usage for object columns: 9.53 MB
Essas informações nos fazem entender que a maior parte da memória é gasta em 78 colunas armazenando valores de objetos. Falaremos mais sobre isso mais tarde, mas agora vamos pensar se podemos melhorar o uso de memória com colunas que armazenam dados numéricos.
Subtipos
Como já dissemos, os pandas representam valores numéricos como estruturas de dados
ndarray
NumPy e os armazenam em blocos contíguos de memória. Esse modelo de armazenamento de dados permite economizar memória e acessar rapidamente valores. Como os pandas representam cada valor do mesmo tipo usando o mesmo número de bytes, e
ndarray
estruturas
ndarray
armazenam informações sobre o número de valores, os pandas podem
ndarray
rápida e com precisão a quantidade de memória consumida por colunas que armazenam valores numéricos.
Muitos tipos de dados nos pandas têm muitos subtipos que podem usar menos bytes para representar cada valor. Por exemplo, o tipo
float
possui os subtipos
float16
,
float64
e
float64
. O número no nome do tipo indica o número de bits que o subtipo usa para representar os valores. Por exemplo, nos subtipos listados abaixo, 2, 4, 8 e 16 bytes são usados respectivamente para armazenamento de dados. A tabela a seguir mostra os subtipos dos tipos de dados mais usados nos pandas.
Uso de memória, bytes
| Número do ponto flutuante
| Inteiro
| Inteiro não assinado
| Data e hora
| Valor booleano
| Object
|
1
| | int8
| uint8
| | bool
| |
2
| float16
| int16
| uint16
| | | |
4
| float32
| int32
| uint32
| | | |
8
| float64
| int64
| uint64
| datetime64
| | |
Capacidade de memória variável
| | | | | | objeto
|
Um valor do tipo
int8
usa 1 byte (8 bits) para armazenar um número e pode representar 256 valores binários (potência de 2 a 8). Isso significa que esse subtipo pode ser usado para armazenar valores no intervalo de -128 a 127 (incluindo 0).
Para verificar os valores mínimo e máximo adequados para armazenamento usando cada subtipo de número inteiro, você pode usar o método
numpy.iinfo()
. Considere um exemplo:
import numpy as np int_types = ["uint8", "int8", "int16"] for it in int_types: print(np.iinfo(it))
Ao executar esse código, obtemos os seguintes dados:
Machine parameters for uint8 --------------------------------------------------------------- min = 0 max = 255 --------------------------------------------------------------- Machine parameters for int8 --------------------------------------------------------------- min = -128 max = 127 --------------------------------------------------------------- Machine parameters for int16 --------------------------------------------------------------- min = -32768 max = 32767 ---------------------------------------------------------------
Aqui você pode prestar atenção à diferença entre os tipos
uint
(número inteiro não assinado) e
int
(número inteiro assinado). Ambos os tipos têm a mesma capacidade, mas, ao armazenar apenas valores positivos em colunas, os tipos não assinados permitem um uso mais eficiente da memória.
Otimização do armazenamento de dados numéricos usando subtipos
A função
pd.to_numeric()
pode ser usada para converter tipos numéricos de forma descendente. Para selecionar colunas inteiras, usamos o método
DataFrame.select_dtypes()
, depois as otimizamos e comparamos o uso de memória antes e depois da otimização.
# , , # , . def mem_usage(pandas_obj): if isinstance(pandas_obj,pd.DataFrame): usage_b = pandas_obj.memory_usage(deep=True).sum() else: # , DataFrame, Series usage_b = pandas_obj.memory_usage(deep=True) usage_mb = usage_b / 1024 ** 2 # return "{:03.2f} MB".format(usage_mb) gl_int = gl.select_dtypes(include=['int']) converted_int = gl_int.apply(pd.to_numeric,downcast='unsigned') print(mem_usage(gl_int)) print(mem_usage(converted_int)) compare_ints = pd.concat([gl_int.dtypes,converted_int.dtypes],axis=1) compare_ints.columns = ['before','after'] compare_ints.apply(pd.Series.value_counts)
Aqui está o resultado de um estudo do consumo de memória:
7.87 MB
1.48 MB
| Para
| Depois
|
uint8
| NaN
| 5.0
|
uint32
| NaN
| 1.0
|
int64
| 6.0
| NaN
|
Como resultado, você pode observar uma queda no uso de memória de 7,9 para 1,5 megabytes, ou seja, reduzimos o consumo de memória em mais de 80%. O impacto geral dessa otimização no
DataFrame
original, no entanto, não é particularmente forte, pois possui muito poucas colunas inteiras.
Vamos fazer o mesmo com colunas contendo números de ponto flutuante.
gl_float = gl.select_dtypes(include=['float']) converted_float = gl_float.apply(pd.to_numeric,downcast='float') print(mem_usage(gl_float)) print(mem_usage(converted_float)) compare_floats = pd.concat([gl_float.dtypes,converted_float.dtypes],axis=1) compare_floats.columns = ['before','after'] compare_floats.apply(pd.Series.value_counts)
O resultado é o seguinte:
100.99 MB
50.49 MB
| Para
| Depois
|
float32
| NaN
| 77,0
|
float64
| 77,0
| NaN
|
Como resultado, todas as colunas que armazenavam números de ponto flutuante com o tipo de dados
float64
agora armazenam números do tipo
float32
, o que nos deu uma redução de 50% no uso de memória.
Crie uma cópia do
DataFrame
original, use essas colunas numéricas otimizadas em vez daquelas que estavam originalmente presentes nela e observe o uso geral da memória após a otimização.
optimized_gl = gl.copy() optimized_gl[converted_int.columns] = converted_int optimized_gl[converted_float.columns] = converted_float print(mem_usage(gl)) print(mem_usage(optimized_gl))
Aqui está o que temos:
861.57 MB
804.69 MB
Embora
DataFrame
significativamente o consumo de memória por colunas que armazenam dados numéricos, em geral, em todo o
DataFrame
, o consumo de memória diminuiu apenas 7%. A otimização do armazenamento de tipos de objetos pode se tornar uma fonte de melhoria muito mais séria de uma situação.
Antes de fazermos essa otimização, veremos mais de perto como as strings são armazenadas nos pandas e comparamos com a maneira como os números são armazenados aqui.
Comparação de mecanismos para armazenar números e seqüências de caracteres
O tipo de
object
representa valores usando objetos de seqüência de caracteres Python. Isso ocorre em parte porque o NumPy não suporta a representação de valores de string ausentes. Como o Python é uma linguagem interpretada de alto nível, ele não fornece ao programador ferramentas para ajustar como os dados são armazenados na memória.
Essa limitação leva ao fato de que as seqüências de caracteres não são armazenadas em fragmentos contíguos da memória; sua representação na memória é fragmentada. Isso leva a um aumento no consumo de memória e a uma desaceleração na velocidade de trabalho com valores de sequência. Cada elemento da coluna que armazena o tipo de dados do objeto, de fato, é um ponteiro que contém o "endereço" no qual o valor real está localizado na memória.
A seguir, é apresentado um diagrama baseado
neste material que compara o armazenamento de dados numéricos usando os tipos de dados NumPy e o armazenamento de strings usando os tipos de dados internos do Python.
Armazenando dados numéricos e de sequênciaAqui você pode lembrar que, em uma das tabelas acima, foi mostrado que uma quantidade variável de memória é usada para armazenar dados de tipos de objetos. Embora cada ponteiro ocupe 1 byte de memória, cada valor específico da string ocupa a mesma quantidade de memória que seria usada para armazenar uma única string no Python. Para confirmar isso, usaremos o método
sys.getsizeof()
. Primeiro, dê uma olhada nas linhas individuais e, em seguida, no objeto pandas da
Series
que armazena os dados da string.
Então, primeiro examinamos as linhas usuais:
from sys import getsizeof s1 = 'working out' s2 = 'memory usage for' s3 = 'strings in python is fun!' s4 = 'strings in python is fun!' for s in [s1, s2, s3, s4]: print(getsizeof(s))
Aqui, os dados de uso da memória são assim:
60
65
74
74
Agora vamos ver como é o uso de strings no objeto
Series
:
obj_series = pd.Series(['working out', 'memory usage for', 'strings in python is fun!', 'strings in python is fun!']) obj_series.apply(getsizeof)
Aqui temos o seguinte:
0 60 1 65 2 74 3 74 dtype: int64
Aqui você pode ver que os tamanhos das linhas armazenadas nos objetos pandas da
Series
são semelhantes aos seus tamanhos ao trabalhar com eles no Python e ao representá-los como entidades separadas.
Otimização do armazenamento de dados de tipo de objeto usando variáveis categóricas
Variáveis categóricas apareceram no pandas versão 0.15. O tipo correspondente,
category
, usa valores inteiros em seus mecanismos internos, em vez dos valores originais armazenados nas colunas da tabela. O Pandas usa um dicionário separado que define a correspondência dos valores inteiro e inicial. Essa abordagem é útil quando as colunas contêm valores de um conjunto limitado. Quando os dados armazenados em uma coluna são convertidos para o tipo de
category
, o pandas usa o subtipo
int
, que permite o uso mais eficiente da memória e é capaz de representar todos os valores exclusivos encontrados na coluna.
Dados de origem e dados categóricos usando o subtipo int8Para entender exatamente onde podemos usar dados categóricos para reduzir o consumo de memória, descobrimos o número de valores exclusivos nas colunas que armazenam os valores dos tipos de objeto:
gl_obj = gl.select_dtypes(include=['object']).copy() gl_obj.describe()
Você pode encontrar o que temos
nesta tabela, na folha
.
Por exemplo, na coluna
day_of_week
, que é o dia da semana em que o jogo foi disputado, existem valores 171907. Entre eles, apenas 7 são únicos. No geral, uma simples olhada neste relatório é suficiente para entender que alguns valores únicos são usados em muitas colunas para representar os dados de aproximadamente 172.000 jogos.
Antes de fazermos a otimização em escala real, vamos selecionar uma coluna que armazena dados do objeto, pelo menos
day_of_week
, e ver o que acontece no programa quando ele é convertido em um tipo categórico.
Como já mencionado, esta coluna contém apenas 7 valores exclusivos. Para convertê-lo em um tipo categórico, usamos o método
.astype()
.
dow = gl_obj.day_of_week print(dow.head()) dow_cat = dow.astype('category') print(dow_cat.head())
Aqui está o que temos:
0 Thu 1 Fri 2 Sat 3 Mon 4 Tue Name: day_of_week, dtype: object 0 Thu 1 Fri 2 Sat 3 Mon 4 Tue Name: day_of_week, dtype: category Categories (7, object): [Fri, Mon, Sat, Sun, Thu, Tue, Wed]
Como você pode ver, embora o tipo da coluna tenha sido alterado, os dados armazenados nela parecem os mesmos de antes. Agora vamos ver o que está acontecendo dentro do programa.
No código a seguir, usamos o atributo
Series.cat.codes
para descobrir quais valores inteiros o tipo de
category
usa para representar cada dia da semana:
dow_cat.head().cat.codes
Conseguimos descobrir o seguinte:
0 4 1 0 2 2 3 1 4 5 dtype: int8
Aqui você pode ver que cada valor exclusivo recebe um valor inteiro e que a coluna agora é do tipo
int8
. Não há valores ausentes, mas se fosse esse o caso, -1 seria usado para indicar esses valores.
Agora vamos comparar o consumo de memória antes e depois de converter a coluna
day_of_week
no tipo de
category
.
print(mem_usage(dow)) print(mem_usage(dow_cat))
Aqui está o resultado:
9.84 MB
0.16 MB
Como você pode ver, foram consumidos inicialmente 9,84 megabytes de memória e, após a otimização, apenas 0,16 megabytes, o que significa uma melhoria de 98% nesse indicador. Observe que o trabalho com esta coluna provavelmente demonstra um dos cenários de otimização mais lucrativos quando apenas 7 valores exclusivos são usados em uma coluna contendo aproximadamente 172.000 elementos.
Embora a idéia de converter todas as colunas para esse tipo de dados pareça atraente, antes de fazer isso, considere os efeitos colaterais negativos dessa conversão. Portanto, o ponto mais negativo dessa transformação é a impossibilidade de executar operações aritméticas em dados categóricos. Isso também se aplica a operações aritméticas comuns e ao uso de métodos como
Series.min()
e
Series.max()
sem primeiro converter os dados em um tipo de número real.
Devemos limitar o uso do tipo de
category
a principalmente colunas que armazenam dados do tipo de
object
, nos quais menos de 50% dos valores são únicos. Se todos os valores em uma coluna forem exclusivos, o uso do tipo de
category
aumentará o nível de uso da memória. Isso se deve ao fato de que na memória você precisa armazenar, além dos códigos de categoria numéricos, os valores originais da string. Detalhes sobre restrições de tipo de
category
podem ser encontrados na
documentação do pandas.
Vamos criar um loop que itere sobre todas as colunas que armazenam dados do tipo de
object
, descobre se o número de valores exclusivos nas colunas excede 50% e, nesse caso, os converte na
category
tipo.
converted_obj = pd.DataFrame() for col in gl_obj.columns: num_unique_values = len(gl_obj[col].unique()) num_total_values = len(gl_obj[col]) if num_unique_values / num_total_values < 0.5: converted_obj.loc[:,col] = gl_obj[col].astype('category') else: converted_obj.loc[:,col] = gl_obj[col]
Agora compare o que aconteceu após a otimização com o que aconteceu antes:
print(mem_usage(gl_obj)) print(mem_usage(converted_obj)) compare_obj = pd.concat([gl_obj.dtypes,converted_obj.dtypes],axis=1) compare_obj.columns = ['before','after'] compare_obj.apply(pd.Series.value_counts)
Temos o seguinte:
752.72 MB
51.67 MB
| Para
| Depois
|
objeto
| 78,0
| NaN
|
categoria
| NaN
| 78,0
|
category
, , , , , , , , .
, , ,
object
, 752 52 , 93%. , . , , , , 891 .
optimized_gl[converted_obj.columns] = converted_obj mem_usage(optimized_gl)
:
'103.64 MB'
. - . ,
datetime
, , , .
date = optimized_gl.date print(mem_usage(date)) date.head()
:
0.66 MB
:
0 18710504 1 18710505 2 18710506 3 18710508 4 18710509 Name: date, dtype: uint32
,
uint32
. -
datetime
, 64 .
datetime
, , , .
to_datetime()
,
format
,
YYYY-MM-DD
.
optimized_gl['date'] = pd.to_datetime(date,format='%Y%m%d') print(mem_usage(optimized_gl)) optimized_gl.date.head()
:
104.29 MB
:
0 1871-05-04 1 1871-05-05 2 1871-05-06 3 1871-05-08 4 1871-05-09 Name: date, dtype: datetime64[ns]
DataFrame
. , , , , , , , . , . , , , . , ,
DataFrame
, .
, .
pandas.read_csv() , . ,
dtype
, , , , — NumPy.
, , . , .
dtypes = optimized_gl.drop('date',axis=1).dtypes dtypes_col = dtypes.index dtypes_type = [i.name for i in dtypes.values] column_types = dict(zip(dtypes_col, dtypes_type)) # 161 , # 10 / # preview = first2pairs = {key:value for key,value in list(column_types.items())[:10]} import pprint pp = pp = pprint.PrettyPrinter(indent=4) pp.pprint(preview) : { 'acquisition_info': 'category', 'h_caught_stealing': 'float32', 'h_player_1_name': 'category', 'h_player_9_name': 'category', 'v_assists': 'float32', 'v_first_catcher_interference': 'float32', 'v_grounded_into_double': 'float32', 'v_player_1_id': 'category', 'v_player_3_id': 'category', 'v_player_5_id': 'category'}
, , .
- :
read_and_optimized = pd.read_csv('game_logs.csv',dtype=column_types,parse_dates=['date'],infer_datetime_format=True) print(mem_usage(read_and_optimized)) read_and_optimized.head()
:
104.28 MB
,
.
,
, , , . pandas 861.6 104.28 , 88% .
, , , . .
optimized_gl['year'] = optimized_gl.date.dt.year games_per_day = optimized_gl.pivot_table(index='year',columns='day_of_week',values='date',aggfunc=len) games_per_day = games_per_day.divide(games_per_day.sum(axis=1),axis=0) ax = games_per_day.plot(kind='area',stacked='true') ax.legend(loc='upper right') ax.set_ylim(0,1) plt.show()
,, 1920- , , 50 , .
, , , 50 , .
, .
game_lengths = optimized_gl.pivot_table(index='year', values='length_minutes') game_lengths.reset_index().plot.scatter('year','length_minutes') plt.show()
, 1940- .
Sumário
pandas, ,
DataFrame
, 90%. :
, , , , , , pandas, , .
Caros leitores! eugene_bb . - , — .
