Como otimizar consumo de memória ao usar Pandas

Os tipos object e int64 prejudicam a performance durante uma análise e, às vezes, podem ser evitados

24 out 2019


Quem usa Pandas numa máquina de 8 GB de RAM conhece bem a frustração de perder uma análise porque o computador trava. E não estamos falando em dezenas de gigabytes de dados: basta o dataset ter uns 800 MB para as chances de travamento serem reais.

Isso porque, apesar de o tamanho do arquivo não ser grande, Pandas demanda memória. E aqui cabe explicar um ponto fundamental: o tamanho do dataset não equivale à exigência de RAM. Há quem diga ser necessário o dobro de RAM em relação do tamanho do arquivo.

Entretanto, Wes McKinney, criador de Pandas, num post em seu blog, ressalta o que chama de “regra de ouro” quando se fala de memória:

My rule of thumb for Pandas is that you should have 5 to 10 times as much RAM as the size of your dataset.

Repare que ele não crava um valor, mas sugere um intervalo: memória de cinco a dez vezes o tamanho o dataset.

Para observar isso de maneira mais aprofundada, vamos trabalhar com um exemplo: os dados da cota parlamentar dos deputados federais dos últimos dez anos. O dataset usado aqui é a agregação dos arquivos .csv divulgados pela Câmara dos Deputados, disponíveis para download no portal. Vamos trabalhar esse dataset numa máquina de 8 GB de RAM.

In [1]:
# Checagem de memória na máquina
!free -m
              total        used        free      shared  buff/cache   available
Mem:           7832        2856        2359         196        2616        4497
Swap:          2047           0        2047
In [2]:
import os
import csv

csv_file = 'data.csv'

# Checagem do tamanho com conversão de bytes em megabytes
size = os.path.getsize(csv_file) / 1024**2

# Checagem de linhas e colunas
with open(csv_file) as file:
    reader = csv.reader(file, delimiter=',')
    ncols = len(next(reader))
    nrows = 0
    for _ in reader:
        nrows += 1

# Obtenção do resultado
print(f'Arquivo: {csv_file}\n\
Tamanho: {round(size, 2)} MB\n\
Linhas: {nrows}\n\
Colunas: {ncols}')
Arquivo: data.csv
Tamanho: 754.91 MB
Linhas: 3484985
Colunas: 30

O arquivo é constituído de 3.484.985 linhas e 30 colunas, e tem pouco mais de 750 MB. Mas são necessários quase 3 GB de RAM —cerca de quatro vezes seu tamanho— para ser trabalhado em Pandas —em grande parte, devido às colunas com valores do tipo object: cada uma delas consome, em média, 203.63 MB de memória.

In [1]:
import pandas as pd

# Leitura convencional do arquivo
df = pd.read_csv('data.csv')

# Checagem de informações com uso de memória geral
df.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3484985 entries, 0 to 3484984
Data columns (total 30 columns):
txNomeParlamentar            object
cpf                          float64
ideCadastro                  float64
nuCarteiraParlamentar        float64
nuLegislatura                int64
sgUF                         object
sgPartido                    object
codLegislatura               int64
numSubCota                   int64
txtDescricao                 object
numEspecificacaoSubCota      int64
txtDescricaoEspecificacao    object
txtFornecedor                object
txtCNPJCPF                   object
txtNumero                    object
indTipoDocumento             int64
datEmissao                   object
vlrDocumento                 float64
vlrGlosa                     float64
vlrLiquido                   float64
numMes                       int64
numAno                       int64
numParcela                   int64
txtPassageiro                object
txtTrecho                    object
numLote                      int64
numRessarcimento             float64
vlrRestituicao               float64
nuDeputadoId                 int64
ideDocumento                 int64
dtypes: float64(8), int64(11), object(11)
memory usage: 2.9 GB
In [2]:
# Checagem de memória consumida por coluna
df.memory_usage(deep=True, index=False)
Out[2]:
txNomeParlamentar            269236856
cpf                           27879880
ideCadastro                   27879880
nuCarteiraParlamentar         27879880
nuLegislatura                 27879880
sgUF                         205474768
sgPartido                    209496629
codLegislatura                27879880
numSubCota                    27879880
txtDescricao                 362762917
numEspecificacaoSubCota       27879880
txtDescricaoEspecificacao    158995761
txtFornecedor                308344599
txtCNPJCPF                   261348406
txtNumero                    234588538
indTipoDocumento              27879880
datEmissao                   256587872
vlrDocumento                  27879880
vlrGlosa                      27879880
vlrLiquido                    27879880
numMes                        27879880
numAno                        27879880
numParcela                    27879880
txtPassageiro                153056118
txtTrecho                    142387243
numLote                       27879880
numRessarcimento              27879880
vlrRestituicao                27879880
nuDeputadoId                  27879880
ideDocumento                  27879880
dtype: int64
In [3]:
# Checagem da média de memória consumida por tipo
for i in list(set(df.dtypes)):
    selected_type = df.select_dtypes(include=[i])
    mean_usage = selected_type.memory_usage(deep=True).mean()
    converted_mean_usage = mean_usage / 1024 ** 2
    print(f'Média de memória consumida por colunas do tipo {i}: {round(converted_mean_usage, 2)} MB')
Média de memória consumida por colunas do tipo float64: 23.63 MB
Média de memória consumida por colunas do tipo object: 203.63 MB
Média de memória consumida por colunas do tipo int64: 24.37 MB

Ou seja, dos cerca de 3 GB de RAM necessários para trabalhar este dataset, mais de 2 GB de RAM são apenas para as 11 colunas contendo valores do tipo object, enquanto cerca de 250 MB são destinados apenas para trabalhar as colunas do tipo int64.

Primeiro gargalo: object

O tipo object é uma representação dos valores em string de Python. E string de Python é um problema quando o assunto é memória.

Numa sequência de valores object, cada elemento tem um ponteiro que leva a um buffer contíguo de outros ponteiros, estes últimos levando às posições na memória. Isso significa que object:

  1. exige duplo trabalho de busca de valores,
  2. procura na memória valores distribuídos de maneira fragmentada.

É diferente, por exemplo, de uma array de NumPy que, assim como uma array de C, tem um ponteiro direto para um buffer contíguo de valores.

Para melhor compreensão, imagine que seu sistema é um motoboy de pizzaria.

A pizzaria pode receber pedidos para entregas em casas vizinhas. Ou seja, o motoboy pode pegar as duas pizzas e fazer apenas uma viagem. Isso é uma array de NumPy.

Ou a pizzaria pode receber pedidos para entregas em casas de lados opostos da cidade. O motoboy pega a primeira pizza, faz uma viagem até a zona sul, retorna à pizzaria para pegar a segunda pizza e segue para a zona norte. Isso é object de Pandas.

Nesse exemplo, a gasolina da moto é a memória do seu computador. Mas existe uma maneira de economizar “combustível”: converter object para category.

O tipo category, como definido na documentação de Pandas, tem uma quantidade limitada e fixa de valores possíveis. Por exemplo: sexo biológico (M e F), tipo sanguíneo (A+, B-, AB+, O- etc.), tamanho de roupa (P, M, G, GG etc.)…

Ao converter object para category, nos bastidores você está convertendo string em algo numérico —ou seja, facilitando a busca do valor na memória.

Vamos ver no nosso dataset quais colunas com object preenchem esses requisitos e podem ser convertidas em category.

In [4]:
# Checagem de colunas do tipo 'object'
for i in df.select_dtypes(include='object'):
    print(f'Coluna: {df[i].name}\tValores únicos: {df[i].nunique()}\n{df[i].unique()}\n')
Coluna: txNomeParlamentar  Valores únicos: 1441
['JAIRO ATAÍDE' 'BISPO GÊ TENUTA' 'JORGINHO MALULY' ... 'JOSEILDO RAMOS'
 'RICARDO PERICAR' 'FABIANO TOLENTINO']

Coluna: sgUF    Valores únicos: 27
['MG' 'SP' 'RJ' 'BA' 'PE' 'RR' 'MS' 'PR' 'SC' 'RS' 'MT' 'ES' 'DF' 'CE'
 'TO' 'GO' 'AP' 'PA' 'AM' 'RO' 'AC' 'AL' 'MA' 'RN' 'PB' 'SE' 'PI' nan]

Coluna: sgPartido   Valores únicos: 41
['DEM' 'PRB' 'PT' 'PTB' 'PSB' 'PMDB' 'PP**' 'PCdoB' 'PSDB' 'PR' 'PPS'
 'PDT' 'PSOL' 'PSC' 'PHS' 'PV' 'PTC' 'PMN' 'PTdoB' nan 'MDB' 'SD'
 'REPUBLICANOS' 'CIDADANIA' 'PROS' 'PSD' 'PP' 'PODE' 'PATRI' 'S.PART.'
 'AVANTE' 'PL' 'PRP' 'PATRIOTA' 'SOLIDARIEDADE' 'PEN' 'PSDC' 'PSL' 'REDE'
 'PPL' 'PRTB' 'NOVO']

Coluna: txtDescricao    Valores únicos: 19
['MANUTENÇÃO DE ESCRITÓRIO DE APOIO À ATIVIDADE PARLAMENTAR'
 'COMBUSTÍVEIS E LUBRIFICANTES.'
 'CONSULTORIAS, PESQUISAS E TRABALHOS TÉCNICOS.'
 'DIVULGAÇÃO DA ATIVIDADE PARLAMENTAR.' 'TELEFONIA' 'SERVIÇOS POSTAIS'
 'ASSINATURA DE PUBLICAÇÕES' 'FORNECIMENTO DE ALIMENTAÇÃO DO PARLAMENTAR'
 'HOSPEDAGEM ,EXCETO DO PARLAMENTAR NO DISTRITO FEDERAL.'
 'LOCAÇÃO DE VEÍCULOS AUTOMOTORES OU FRETAMENTO DE EMBARCAÇÕES'
 'Emissão Bilhete Aéreo' 'PASSAGENS AÉREAS'
 'SERVIÇO DE SEGURANÇA PRESTADO POR EMPRESA ESPECIALIZADA.'
 'SERVIÇO DE TÁXI, PEDÁGIO E ESTACIONAMENTO'
 'LOCAÇÃO OU FRETAMENTO DE VEÍCULOS AUTOMOTORES'
 'LOCAÇÃO OU FRETAMENTO DE AERONAVES'
 'LOCAÇÃO OU FRETAMENTO DE EMBARCAÇÕES'
 'PASSAGENS TERRESTRES, MARÍTIMAS OU FLUVIAIS'
 'PARTICIPAÇÃO EM CURSO, PALESTRA OU EVENTO SIMILAR']

Coluna: txtDescricaoEspecificacao   Valores únicos: 4
[nan 'Veículos Automotores' 'Sem especificações' 'Embarcações' 'Aeronaves']

Coluna: txtFornecedor   Valores únicos: 191212
['CEMIG' 'COOPERBH-TAXI' 'COPASA' ... 'AUTO SERVICO BARREIRA S LTDA'
 'Posto Planetário LTDA' 'Liberio joao de santana & cia ltda']

Coluna: txtCNPJCPF  Valores únicos: 92209
['069.811.800/0011-6' '042.763.570/0015-8' '172.811.060/0010-3' ...
 '295.372.890/0016-4' '286.268.100/0017-7' '653.567.270/0015-1']

Coluna: txtNumero   Valores únicos: 2036653
['003068310' '003110535' '003153516' ... '763733' 'Bilhete: IK7DPX'
 'Bilhete: WF2HSM']

Coluna: datEmissao  Valores únicos: 310769
['2010-04-16T00:00:00' '2010-05-17T00:00:00' '2010-01-15T00:00:00' ...
 '2019-07-24T11:49:38' '2019-08-09T12:38:47' '2019-10-08T16:55:05']

Coluna: txtPassageiro   Valores únicos: 34380
[nan 'VIEIRA/JAIRO' 'LIMA/JANIO' ... 'ALUA CARMO DE MOURA'
 'RICARDO PERICAR' 'FABIANO TOLENTINO']

Coluna: txtTrecho   Valores únicos: 18981
[nan 'BSBG3CNF' 'CNFG3BSB' ... 'BSB/MAO/FOR/REC' 'VCP/BSB/SSA'
 'BSB/GRU/VIX/CGH/BSB']

A coluna sgUF tem somente 27 valores únicos, além de outros nulos; sgPartido, 41 valores únicos, mais nulos; txtDescricao, 19; txtDescricaoEspecificacao, quatro e outros nulos. Essas colunas todas podem ser convertidas, pois contêm valores fixos e limitados.

In [5]:
def mem_usage(pandas_object):
    """Calcula o uso de memória do dataframe ou da série"""
    if isinstance(pandas_object, pd.DataFrame):
        usage = pandas_object.memory_usage(deep=True).sum()
    else:
        usage = pandas_object.memory_usage(deep=True)
    converted_usage = usage / 1024 ** 2
    return f'{round(converted_usage, 2)} MB'
In [6]:
# Criação de dataframe vazio
new_df = pd.DataFrame()

# Conversão das colunas
for col in df.columns:
    if col in ['sgUF', 'sgPartido', 'txtDescricao', 'txtDescricaoEspecificacao']:
        new_df.loc[:,col] = df[col].astype('category')
    else:
        new_df.loc[:,col] = df[col]

# Resultado
print(f'Memória consumida antes: {mem_usage(df)}\n\
Memória consumida depois: {mem_usage(new_df)}')
Memória consumida antes: 3053.99 MB
Memória consumida depois: 2082.63 MB
In [7]:
#Criação de dataframe com resultados
previous = pd.DataFrame(df.memory_usage(deep=True), columns=['Antes'])
now = pd.DataFrame(new_df.memory_usage(deep=True), columns=['Depois'])
result = pd.concat([previous, now], axis=1)
result['Variação (%)'] = ((result['Depois'] / result['Antes']) - 1) * 100

# Resultado das colunas convertidas
result[result['Variação (%)'] != 0]
Out[7]:
Antes Depois Variação (%)
sgUF 205474768 3487858 -98.302537
sgPartido 209496629 3488772 -98.334688
txtDescricao 458518562 3488536 -99.239172
txtDescricaoEspecificacao 158995761 3485503 -97.807801

A memória consumida por essas colunas teve redução superior a 95%, e a memória total caiu de 3 GB para 2 GB. A mudança mais significativa foi em txtDescricao, superior a 99%.

O que aconteceu nos bastidores: a conversão de object para category fez com que o valor passasse a ser interpretado como integer —e, agora sabemos, números consomem menos memória.

Inclusive é possível ver o integer de cada valor categórico:

In [8]:
# Checagem dos valores de 'txtDescricao' convertidos para integer
dict(zip(new_df['txtDescricao'], new_df['txtDescricao'].cat.codes))
Out[8]:
{'MANUTENÇÃO DE ESCRITÓRIO DE APOIO À ATIVIDADE PARLAMENTAR': 11,
 'COMBUSTÍVEIS E LUBRIFICANTES.': 1,
 'CONSULTORIAS, PESQUISAS E TRABALHOS TÉCNICOS.': 2,
 'DIVULGAÇÃO DA ATIVIDADE PARLAMENTAR.': 3,
 'TELEFONIA': 18,
 'SERVIÇOS POSTAIS': 17,
 'ASSINATURA DE PUBLICAÇÕES': 0,
 'FORNECIMENTO DE ALIMENTAÇÃO DO PARLAMENTAR': 5,
 'HOSPEDAGEM ,EXCETO DO PARLAMENTAR NO DISTRITO FEDERAL.': 6,
 'LOCAÇÃO DE VEÍCULOS AUTOMOTORES OU FRETAMENTO DE EMBARCAÇÕES': 7,
 'Emissão Bilhete Aéreo': 4,
 'PASSAGENS AÉREAS': 13,
 'SERVIÇO DE SEGURANÇA PRESTADO POR EMPRESA ESPECIALIZADA.': 15,
 'SERVIÇO DE TÁXI, PEDÁGIO E ESTACIONAMENTO': 16,
 'LOCAÇÃO OU FRETAMENTO DE VEÍCULOS AUTOMOTORES': 10,
 'LOCAÇÃO OU FRETAMENTO DE AERONAVES': 8,
 'LOCAÇÃO OU FRETAMENTO DE EMBARCAÇÕES': 9,
 'PASSAGENS TERRESTRES, MARÍTIMAS OU FLUVIAIS': 14,
 'PARTICIPAÇÃO EM CURSO, PALESTRA OU EVENTO SIMILAR': 12}

Cada valor recebeu um integer de referência, de 0 a 18. E sempre que o valor for repetido, o que se repetirá será um integer, não um object.

Mas atenção: converter object para category pode levar a coluna a consumir mais memória, caso haja muitos valores únicos —ou seja, sem repetição de valor. É o que diz a documentação:

Note: If the number of categories approaches the length of the data, the Categorical will use nearly the same or more memory than an equivalent object dtype representation.

No dataset que estamos vendo, isso ocorre com txtNumero, por exemplo:

In [9]:
# Exemplo de conversão ruim em 'txtNumero'
as_object = new_df['txtNumero']
as_category = new_df['txtNumero'].astype('category')

# Resultado
print(f'Coluna como object: {as_object.memory_usage(deep=True)}\n\
Coluna como category: {as_category.memory_usage(deep=True)}\n\
Diferença: {round(((as_category.memory_usage(deep=True)-as_object.memory_usage(deep=True)) / 1024 ** 2), 2)} MB')
Coluna como object: 234588666
Coluna como category: 240993720
Diferença: 6.11 MB

Cabe, pois, certa parcimônia: não vale a pena converter tudo que é object em category. E lembrar sempre: category vale para valores fixos (ou seja, permanentes, que aparecem de maneira recorrente) e limitados (sem tanta variação).

Segundo gargalo: int64

Vimos que category “traduz” o tipo object para o tipo integer, o que consome menos memória. Em nosso dataset, quando convertemos quatro colunas para category, obtivemos mais de 30% de economia de memória.

In [1]:
import pandas as pd

# Leitura do arquivo sem tipagem
raw_df = pd.read_csv('data.csv')

# Resultado
print(raw_df.info(memory_usage='deep'))
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3484985 entries, 0 to 3484984
Data columns (total 30 columns):
txNomeParlamentar            object
cpf                          float64
ideCadastro                  float64
nuCarteiraParlamentar        float64
nuLegislatura                int64
sgUF                         object
sgPartido                    object
codLegislatura               int64
numSubCota                   int64
txtDescricao                 object
numEspecificacaoSubCota      int64
txtDescricaoEspecificacao    object
txtFornecedor                object
txtCNPJCPF                   object
txtNumero                    object
indTipoDocumento             int64
datEmissao                   object
vlrDocumento                 float64
vlrGlosa                     float64
vlrLiquido                   float64
numMes                       int64
numAno                       int64
numParcela                   int64
txtPassageiro                object
txtTrecho                    object
numLote                      int64
numRessarcimento             float64
vlrRestituicao               float64
nuDeputadoId                 int64
ideDocumento                 int64
dtypes: float64(8), int64(11), object(11)
memory usage: 2.9 GB
None
In [2]:
# Leitura do arquivo com tipagem de 'category'
df = pd.read_csv('data.csv',
                 dtype={
                     'sgUF': 'category',
                     'sgPartido': 'category',
                     'txtDescricao': 'category',
                     'txtDescricaoEspecificacao': 'category'
                 })

# Resultado
print(df.info(memory_usage='deep'))
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3484985 entries, 0 to 3484984
Data columns (total 30 columns):
txNomeParlamentar            object
cpf                          float64
ideCadastro                  float64
nuCarteiraParlamentar        float64
nuLegislatura                int64
sgUF                         category
sgPartido                    category
codLegislatura               int64
numSubCota                   int64
txtDescricao                 category
numEspecificacaoSubCota      int64
txtDescricaoEspecificacao    category
txtFornecedor                object
txtCNPJCPF                   object
txtNumero                    object
indTipoDocumento             int64
datEmissao                   object
vlrDocumento                 float64
vlrGlosa                     float64
vlrLiquido                   float64
numMes                       int64
numAno                       int64
numParcela                   int64
txtPassageiro                object
txtTrecho                    object
numLote                      int64
numRessarcimento             float64
vlrRestituicao               float64
nuDeputadoId                 int64
ideDocumento                 int64
dtypes: category(4), float64(8), int64(11), object(7)
memory usage: 2.0 GB
None

Bastante efetiva, a conversão de tipos não é a única ação que pode ser tomada para otimizar memória. E aqui entra uma espécie de downgrade de integer.

Quando Pandas reconhece uma sequência de números inteiros, ela automaticamente atribui a cada elemento o subtipo int64 —ou seja, integer de 64 bits. Segundo a documentação,

By default integer types are int64 and float types are float64, regardless of platform (32-bit or 64-bit).

Aliás, a partir da linha 421 de parsers.pyx, do código-fonte de Pandas, está descrita a ordem de tipos usada para avaliar um valor durante a leitura inicial do dataset:

[...]
421        dtype_order = ['int64', 'float64', 'bool', 'object']
422        if quoting == QUOTE_NONNUMERIC:
423            # consistent with csv module semantics, cast all to float
424            dtype_order = dtype_order[1:]
425        self.dtype_cast_order = [np.dtype(x) for x in dtype_order]
[...]

Comparando com os demais subtipos de integer, int64 é imenso. Cada valor consome 64 bits na memória —ou 8 bytes. Em contraste, int8, por exemplo, consome 8 bits —1 byte.

De forma simplista, isso significa que o número 1, por exemplo, pode ocupar 1 byte (se for int8) ou 8 bytes (caso seja int64). O mesmo número pode pesar oito vezes seu peso original na memória!

Isso não significa que todo número possa ter essa variação. Os subtipos de integer acomodam intervalos distintos de números. Vamos ver isso com NumPy, que empresta a Pandas muitas das classes que tratam de tipos:

In [1]:
import numpy as np

int_subtypes = ['int8', 'int16', 'int32', 'int64',
                'uint8', 'uint16', 'uint32', 'uint64']

for i in int_subtypes:
    print(np.iinfo(i))
Machine parameters for int8
---------------------------------------------------------------
min = -128
max = 127
---------------------------------------------------------------

Machine parameters for int16
---------------------------------------------------------------
min = -32768
max = 32767
---------------------------------------------------------------

Machine parameters for int32
---------------------------------------------------------------
min = -2147483648
max = 2147483647
---------------------------------------------------------------

Machine parameters for int64
---------------------------------------------------------------
min = -9223372036854775808
max = 9223372036854775807
---------------------------------------------------------------

Machine parameters for uint8
---------------------------------------------------------------
min = 0
max = 255
---------------------------------------------------------------

Machine parameters for uint16
---------------------------------------------------------------
min = 0
max = 65535
---------------------------------------------------------------

Machine parameters for uint32
---------------------------------------------------------------
min = 0
max = 4294967295
---------------------------------------------------------------

Machine parameters for uint64
---------------------------------------------------------------
min = 0
max = 18446744073709551615
---------------------------------------------------------------

Observe que há duas formas de integer:

  1. aquela que comporta números positivos e negativos, além de zero (chamada signed integer, ou int);
  2. aquela que não compreende números negativos (chamada unsigned integer, ou uint).

Note também que, apesar dessa diferença, a capacidade de armazenamento se equivale:

Subtipo Tamanho em bytes Valor mínimo Valor máximo Total de valores Total de valores usando bits
int8 1 -128 127 256 28
uint8 1 0 255 256 28
int16 2 -32768 32767 65536 216
uint16 2 0 65535 65536 216
int32 4 -2147483648 2147483647 4294967296 232
uint32 4 0 4294967295 4294967296 232
int64 8 -9223372036854775808 9223372036854775807 18446744073709551616 264
uint64 8 0 18446744073709551615 18446744073709551616 264

Agora que sabemos sobre a existência de subtipos de integer, cada um com seu intervalo de valores e consumo de memória, a pergunta que resta é: quando, em toda sua vida, você teve de fazer operações com quintilhões?

Pois é. Creio que nunca. (Ou quase nunca, vá lá.)

Temos, portanto, que int64, o tipo padrão de números inteiros usado em Pandas, é agressivo no consumo de memória. No nosso dataset, por exemplo, cada coluna com valores do tipo int64 consome mais de 26 MB.

In [3]:
# Checagem de consumo de memória em cada coluna 'int64'
for col in df.select_dtypes(include='int64'):
    mean_usage = df[col].memory_usage(deep=True)
    converted_mean_usage = mean_usage / 1024 ** 2
    print(f'Coluna: {df[col].name}\nMin, Max: {df[col].min()}, {df[col].max()}\n\
Tamanho: {converted_mean_usage}\n')
Coluna: nuLegislatura
Min, Max: 2007, 2019
Tamanho: 26.58844757080078

Coluna: codLegislatura
Min, Max: 53, 56
Tamanho: 26.58844757080078

Coluna: numSubCota
Min, Max: 1, 999
Tamanho: 26.58844757080078

Coluna: numEspecificacaoSubCota
Min, Max: 0, 4
Tamanho: 26.58844757080078

Coluna: indTipoDocumento
Min, Max: 0, 4
Tamanho: 26.58844757080078

Coluna: numMes
Min, Max: 1, 12
Tamanho: 26.58844757080078

Coluna: numAno
Min, Max: 2010, 2019
Tamanho: 26.58844757080078

Coluna: numParcela
Min, Max: 0, 1
Tamanho: 26.58844757080078

Coluna: numLote
Min, Max: 0, 1643257
Tamanho: 26.58844757080078

Coluna: nuDeputadoId
Min, Max: 12, 3456
Tamanho: 26.58844757080078

Coluna: ideDocumento
Min, Max: 0, 6941092
Tamanho: 26.58844757080078

Se nossa ideia é buscar otimização, seria melhor rever cada coluna de integer em nosso dataset, observar os valores mínimo e máximo, e adequar o subtipo de integer à demanda específica da coluna.

Por exemplo, a coluna nuLegislatura vai de 2007 a 2019, e caberia com folga nos subtipos int16 ou uint16; já codLegislatura, com valores de 53 a 56, poderia ser tipado como int8 ou uint8… Olhando bem, nenhuma coluna precisa ser int64!

Em vez de checar coluna por coluna, é possível fazer isso numa tacada só, com a função pandas.to_numeric e especificando downcast como argumento. Isso fará com que Pandas “reavalie” as colunas com valores numéricos (no nosso caso, integer) e use o int ou uint mais adequado para a faixa entre os valores mínimo e máximo.

In [4]:
# Tipagem em cada coluna 'int' após 'downcast'
for col in df.select_dtypes(include='int64'):
    df[col] = pd.to_numeric(df[col], downcast='integer')
    print(f'Coluna: {df[col].name}\tTipo: {df[col].dtype}\n')
Coluna: nuLegislatura  Tipo: int16

Coluna: codLegislatura  Tipo: int8

Coluna: numSubCota  Tipo: int16

Coluna: numEspecificacaoSubCota Tipo: int8

Coluna: indTipoDocumento  Tipo: int8

Coluna: numMes  Tipo: int8

Coluna: numAno  Tipo: int16

Coluna: numParcela  Tipo: int8

Coluna: numLote Tipo: int32

Coluna: nuDeputadoId  Tipo: int16

Coluna: ideDocumento  Tipo: int32

Vemos que já não temos mais int64 na lista. Isso se reflete no consumo de memória:

In [5]:
for col in df:
    mean_usage = df[col].memory_usage(deep=True)
    converted_mean_usage = mean_usage / 1024 ** 2
    print(f'Coluna: {df[col].name}\nTamanho: {converted_mean_usage}\nTipo: {df[col].dtype}\n')
Coluna: txNomeParlamentar
Tamanho: 256.76439666748047
Tipo: object

Coluna: cpf
Tamanho: 26.58844757080078
Tipo: float64

Coluna: ideCadastro
Tamanho: 26.58844757080078
Tipo: float64

Coluna: nuCarteiraParlamentar
Tamanho: 26.58844757080078
Tipo: float64

Coluna: nuLegislatura
Tamanho: 6.64720344543457
Tipo: int16

Coluna: sgUF
Tamanho: 3.3264026641845703
Tipo: category

Coluna: sgPartido
Tamanho: 3.3272743225097656
Tipo: category

Coluna: codLegislatura
Tamanho: 3.323662757873535
Tipo: int8

Coluna: numSubCota
Tamanho: 6.64720344543457
Tipo: int16

Coluna: txtDescricao
Tamanho: 3.3270492553710938
Tipo: category

Coluna: numEspecificacaoSubCota
Tamanho: 3.323662757873535
Tipo: int8

Coluna: txtDescricaoEspecificacao
Tamanho: 3.3242111206054688
Tipo: category

Coluna: txtFornecedor
Tamanho: 294.0604467391968
Tipo: object

Coluna: txtCNPJCPF
Tamanho: 249.2413845062256
Tipo: object

Coluna: txtNumero
Tamanho: 223.7211856842041
Tipo: object

Coluna: indTipoDocumento
Tamanho: 3.323662757873535
Tipo: int8

Coluna: datEmissao
Tamanho: 244.70138549804688
Tipo: object

Coluna: vlrDocumento
Tamanho: 26.58844757080078
Tipo: float64

Coluna: vlrGlosa
Tamanho: 26.58844757080078
Tipo: float64

Coluna: vlrLiquido
Tamanho: 26.58844757080078
Tipo: float64

Coluna: numMes
Tamanho: 3.323662757873535
Tipo: int8

Coluna: numAno
Tamanho: 6.64720344543457
Tipo: int16

Coluna: numParcela
Tamanho: 3.323662757873535
Tipo: int8

Coluna: txtPassageiro
Tamanho: 145.96581077575684
Tipo: object

Coluna: txtTrecho
Tamanho: 135.7911787033081
Tipo: object

Coluna: numLote
Tamanho: 13.29428482055664
Tipo: int32

Coluna: numRessarcimento
Tamanho: 26.58844757080078
Tipo: float64

Coluna: vlrRestituicao
Tamanho: 26.58844757080078
Tipo: float64

Coluna: nuDeputadoId
Tamanho: 6.64720344543457
Tipo: int16

Coluna: ideDocumento
Tamanho: 13.29428482055664
Tipo: int32

Antes de reduzir o subtipo, cada coluna de integer consumia mais de 26 MB; agora, não passa de 13 MB, mas a maioria fica entre 3 MB e 6 MB.

No geral, 200 MB de memória foram economizados apenas com pandas.to_numeric e downcast.

In [6]:
# Checagem de tipos no estado final
df.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3484985 entries, 0 to 3484984
Data columns (total 30 columns):
txNomeParlamentar            object
cpf                          float64
ideCadastro                  float64
nuCarteiraParlamentar        float64
nuLegislatura                int16
sgUF                         category
sgPartido                    category
codLegislatura               int8
numSubCota                   int16
txtDescricao                 category
numEspecificacaoSubCota      int8
txtDescricaoEspecificacao    category
txtFornecedor                object
txtCNPJCPF                   object
txtNumero                    object
indTipoDocumento             int8
datEmissao                   object
vlrDocumento                 float64
vlrGlosa                     float64
vlrLiquido                   float64
numMes                       int8
numAno                       int16
numParcela                   int8
txtPassageiro                object
txtTrecho                    object
numLote                      int32
numRessarcimento             float64
vlrRestituicao               float64
nuDeputadoId                 int16
ideDocumento                 int32
dtypes: category(4), float64(8), int16(4), int32(2), int8(5), object(7)
memory usage: 1.8 GB
In [7]:
# Checagem de memória consumida por coluna em estado final
df.memory_usage(deep=True)
Out[7]:
Index                              128
txNomeParlamentar            269236856
cpf                           27879880
ideCadastro                   27879880
nuCarteiraParlamentar         27879880
nuLegislatura                  6969970
sgUF                           3487858
sgPartido                      3488772
codLegislatura                 3484985
numSubCota                     6969970
txtDescricao                   3488536
numEspecificacaoSubCota        3484985
txtDescricaoEspecificacao      3485560
txtFornecedor                308344599
txtCNPJCPF                   261348406
txtNumero                    234588538
indTipoDocumento               3484985
datEmissao                   256587872
vlrDocumento                  27879880
vlrGlosa                      27879880
vlrLiquido                    27879880
numMes                         3484985
numAno                         6969970
numParcela                     3484985
txtPassageiro                153056118
txtTrecho                    142387243
numLote                       13939940
numRessarcimento              27879880
vlrRestituicao                27879880
nuDeputadoId                   6969970
ideDocumento                  13939940
dtype: int64

Agora, o nosso dataset não precisa mais de 3 GB para ser processado, mas somente 1,8 GB —redução de 40% até aqui.