Blog

Postado em em 10 de junho de 2024

Como Otimizar DataFrames do Pandas

Aprenda como otimizar DataFrames do Pandas, um processo crucial para diminuir o uso de memória e aumentar a velocidade de processamento dos seus dados.

Caso prefira esse conteúdo no formato de vídeo-aula, assista ao vídeo abaixo ou acesse o nosso canal do YouTube!

Para receber por e-mail o(s) arquivo(s) utilizados na aula, preencha:

Como Otimizar DataFrames do Pandas

Nesta aula, vou te mostrar como otimizar DataFrames do Pandas! Este é um assunto muito importante, pois com as técnicas que irei te ensinar você será capaz de diminuir o uso de memória do seu DataFrame e a velocidade de processamento dele.

Isso é especialmente crucial ao lidar com grandes bases de dados. Ter uma redução de 50%, por exemplo, já pode fazer uma diferença enorme no seu projeto.

Veremos como realizar operações mais rápidas com DataFrames, aplicando otimizações como a alteração e conversão de tipos de dados no Pandas.

Então, faça o download do material disponível e venha comigo aprender como diminuir o tamanho que o DataFrame ocupa na memória e aumentar sua velocidade de processamento.

Visualizando a Base de Dados

Ao longo desta aula, utilizaremos uma base de dados que contém as transações comerciais de uma empresa.

Nela, temos informações como ID do cliente, data da transação, categoria do produto, idade, gênero, preço do produto e quantidade comprada.

Vamos gerar nosso DataFrame utilizando o Pandas e visualizar as informações da nossa base de dados.

import pandas as pd

BASE = "./dados/dados_transacoes.csv"
df = pd.read_csv(BASE)

df.head()

Visualizando a Base de Dados
df.info()

Visualizando Informações da Base de Dados

Com o método info, conseguimos verificar o número de entradas, a presença ou ausência de valores nulos, os tipos de dados e o uso de memória do DataFrame.

Esse DataFrame ocupa uma quantidade significativa de memória (53.4 MB), considerando que é uma base de dados simplificada. Em uma base de dados completa, esse uso de memória seria ainda maior.

Além disso, durante uma análise de dados, costumamos trabalhar com mais de um DataFrame. Portanto, otimizar até mesmo bases simplificadas é um procedimento importante para reduzir o uso de memória do seu computador.

Método Describe

Outro ponto importante ao trabalhar com DataFrames é utilizar o método describe.

Esse método oferece informações estatísticas do DataFrame, com contagem dos elementos não nulos (count), média (mean), desvio padrão (std), mínimo (min), quartis (25%50%75%) e máximos (max).

df.describe()

Método Describe

Essa verificação é crucial para garantir que, após realizarmos as transformações necessárias para otimizar o DataFrame, a natureza e a distribuição dos dados sejam mantidas.

Podemos também analisar os valores não numéricos passando o parâmetro exclude=”number” para o método describe.

df.describe(exclude="number")

Método Describe colunas não numéricas

Com isso, temos uma descrição inicial dos nossos dados.

Operações de Agrupamento – GroupBy

Uma operação muito comum que costumamos fazer com Pandas, para análises mais detalhadas dos dados, são as operações de agrupamento utilizando o GroupBy.

Por exemplo, podemos agrupar os produtos por categoria e calcular a média de preço dentro de cada uma.

df.groupby("categoria_produto")["preco"].mean()

Operações de Agrupamento

Também podemos agrupar os produtos por categoria e gênero do comprador para uma análise comparativa, verificando se esse fator influencia na média de preços.

df.groupby(["categoria_produto", "genero"])["preco"].mean()

Operações de Agrupamento

Esses são apenas alguns exemplos de análises que podemos realizar com este DataFrame. Cada uma dessas etapas pode ser otimizada com as modificações adequadas.

Comando %timeit do Jupyter Notebook

Para avaliar o tempo médio gasto nas operações sem qualquer tratamento ou modificação, podemos usar o comando %timeit do Jupyter Notebook.

Este comando cronometra o tempo de execução de um código, executando-o várias vezes e apresentando a média de tempo gasto.

%timeit df.groupby("categoria_produto")["preco"].mean()
%timeit df.groupby(["categoria_produto", "genero"])["preco"].mean()

Comando %timeit do Jupyter Notebook

Observe que, para cada linha de código, o comando %timeit foi executado 7 vezes (7 runs), e em cada uma dessas 7 vezes, o código foi executado 10 vezes (10 loops).

O tempo médio obtido para o primeiro agrupamento foi de 47 milissegundos, com uma variação de 542 microssegundos. O segundo agrupamento teve um tempo médio de 106 milissegundos, com uma variação de 1.1 milissegundos.

Esses tempos já são bastante baixos, mas é possível melhorar ainda mais a velocidade de execução deles. Em cenários onde trabalhamos com múltiplas bases de dados ou bases de dados mais complexas, essa otimização se torna ainda mais relevante.

Criando uma Cópia do DataFrame – DataFrame Otimizado

Para mantermos a base original para comparação com a versão otimizada, vamos criar um novo DataFrame que será uma cópia do original.

df_otimizado = df.copy()
df_otimizado.info()

Criando uma Cópia do DataFrame - DataFrame Otimizado

Perceba que temos a mesma estrutura, quantidade de dados, tipos e uso de memória que tínhamos no DataFrame original.

Otimização do DataFrame – Data

Ao otimizar um DataFrame, um dos principais pontos é trabalhar com os tipos de dados presentes nele. Ajustar os tipos de dados é crucial para melhorar o desempenho do DataFrame.

Vamos começar ajustando a coluna data_transacao. Atualmente, os dados nessa coluna estão no formato object (texto). Vamos convertê-los para o tipo datetime utilizando a função to_datetime.

Passaremos para essa função a coluna que queremos converter para data e o parâmetro format, com o formato da data que queremos representar: dia/mês/ano (%d/%m/%Y).

# coluna data_transacao para datetime considerando o formato %d/%m/%Y
df_otimizado["data_transacao"] = pd.to_datetime(df_otimizado["data_transacao"], format="%d/%m/%Y")

df_otimizado.info()

Alterando o tipo da coluna para data

Converter esses dados de object para datetime nos permite uma manipulação mais eficiente dos dados.

Apesar de não representar um ganho direto no desempenho, essa alteração facilita a extração de informações relevantes, simplificando análises futuras.

Por exemplo, no formato datetime, podemos facilmente acessar separadamente as informações de dias, meses e anos presentes nessa coluna.

df_otimizado["data_transacao"].dt.year

Acessando informação dos anos

Otimização do DataFrame – Categorias

Sempre que utilizamos o GroupBy, buscamos colunas com poucos valores únicos para realizar o agrupamento dos dados.

Dessa forma, podemos transformar o tipo das colunas categoria_produto e genero em tipos categóricos.

# colunas categoria_produto e genero para tipos categóricos

df_otimizado["categoria_produto"] = df_otimizado["categoria_produto"].astype("category")

df_otimizado["genero"] = df_otimizado["genero"].astype("category")

df_otimizado.info()

Otimização do DataFrame – Tipos Categóricos

Perceba que essa mudança já altera a memória utilizada pelo nosso DataFrame, pois as categorias que antes eram armazenadas como textos agora são convertidas e armazenadas internamente como números, que ocupam menos espaço na memória.

Espaço que os Números Ocupam na Memória

Agora veremos mais a fundo a diferença no espaço que os números de um DataFrame ocupam na memória. Dentre os valores numéricos do nosso DataFrame, temos os tipos datetime64, int64 e float64.

Pode ser que, dependendo do seu computador, ao lado dos tipos dos números apareça 32 ao invés de 64. Esse número se refere ao número de bits, que é a unidade de memória ocupada no computador. Diferentes tipos de números ocupam diferentes espaços de memória.

A linguagem Python foi construída em cima de outra linguagem de programação, a linguagem C. Nessa linguagem, foram definidos diferentes tipos de números inteiros separados por faixas de valores, dependendo do espaço de memória disponível para armazená-los.

Esses valores podem ser sem sinal ou com sinal.

Memória números inteiros

O tamanho de bytes e bits refere-se ao espaço de memória que aquele número ocupa.

A faixa de valores sem sinal são números que só podem ser positivos ou zero. Eles utilizam todos os bits para representar valores positivos.

A faixa de valores com sinal representa os números que podem ser positivos ou negativos.

Por exemplo, o tipo byte ocupa o tamanho 1 byte ou 8 bits de memória e pode representar valores entre 0 e 255 (sem sinal) ou -128 e 127 (com sinal).

Perceba, observando a tabela, que o inteiro que ocupa mais espaço é justamente o inteiro de 64 bits (int64).

No entanto, dentro do nosso DataFrame, o valor máximo para a coluna idade é 70, enquanto para a coluna quantidade é 9.

Método Describe

Isso significa que não precisamos utilizar um inteiro de 64 bits para armazenar esses tipos de informações. Dessa forma, podemos alterar o tipo numérico dessas colunas para que elas passem a ocupar menos espaço em memória.

Essa prática de reduzir o espaço ocupado por números inteiros ou floats em memória para otimizar as operações é chamada de downcast.

Os números floats funcionam de forma semelhante aos números inteiros, mas possuem diferentes espaços reservados em memória para cada parte deles (parte inteira, decimal e expoente). Esse espaço pode ser de 16 bits, 32 bits ou 64 bits.

Float 16 bits
Float 32 bits
Float 64 bits
Tabela de Floats e memória

Cada um desses tipos float também pode armazenar uma faixa de valores específica. E, assim como os dados presentes nas colunas idade e quantidade, os valores presentes na coluna preco também não são muito elevados, sendo o valor máximo de 5000.00.

O Pandas por padrão sempre reserva o maior espaço possível de memória, para estar preparado para qualquer tipo de operação que você possa querer fazer.

Dessa forma, precisamos fazer o downcast para que nossas colunas passem a ter um tipo de dado mais adequado, economizando memória e melhorando a performance.

Downcast – Alterando os Tipos de Dados

É possível alterar os tipos de dados em um DataFrame com Pandas manualmente ou utilizando ferramentas internas do próprio Pandas para realizar essa otimização.

Ao utilizarmos as ferramentas do Pandas, torna-se mais seguro e prático modificar os tipos de dados sem a necessidade de memorizar os intervalos específicos.

O primeiro passo será selecionar todas as colunas do tipo float64 e int64, armazenando-as em duas variáveis distintas. Para isso, utilizaremos a função select_dtypes, pegando os nomes das colunas (.columns).

# selecionando colunas numéricas por tipo
colunas_float = df_otimizado.select_dtypes(include="float64").columns
colunas_int = df_otimizado.select_dtypes(include="int64").columns

colunas_float, colunas_int

Colunas do tipo Int e Float

Agora podemos selecionar apenas as colunas float e int do nosso DataFrame otimizado (df_otimizado) e aplicar sobre elas a função pd.to_numeric do Pandas, passando a instrução downcast com o tipo de dado correspondente a cada coluna.

df_otimizado[colunas_float] = df_otimizado[colunas_float].apply(pd.to_numeric, downcast="float")
df_otimizado[colunas_int] = df_otimizado[colunas_int].apply(pd.to_numeric, downcast="integer")

Isso forçará o menor tipo float para as colunas do tipo float e o menor tipo de inteiro para as colunas do tipo inteiro.

Feito isso, podemos verificar a mudança nos formatos das colunas através do método .info().

df_otimizado.info()

Info após as alterações

Perceba que as colunas idade e quantidade passaram a ser int8 e a coluna preco passou a ser float32, que é o menor tipo float disponível para o Pandas. Quanto ao uso de memória, veja que agora reduzimos para 22.9 MB, uma redução bastante significativa.

No exemplo desta aula, o DataFrame inicial ocupava apenas 53.4 MB. Porém, em práticas reais no mercado de trabalho, podemos encontrar DataFrames que ocupam até gigabytes de espaço de memória.

Essas práticas são uma boa alternativa para diminuir significativamente o uso de memória do computador, tornando os procedimentos mais rápidos e eficientes.

Podemos inclusive visualizar individualmente o uso de memória de cada coluna presente no nosso DataFrame. Podemos fazer isso utilizando o método memory_usage().

df_otimizado.memory_usage()

memory_usage()

Método Describe – Verificando as Informações

Outro ponto importante após realizarmos todos esses procedimentos e transformações no nosso DataFrame é garantir a integridade dos dados.

Para checar se não houve perda de informações, podemos utilizar o método describe para o DataFrame original e para o DataFrame otimizado e comparar os dois resultados.

df.describe()

Método Describe – Verificando as Informações - Base Original
df_otimizado.describe()

Método Describe – Verificando as Informações - Base Otimizada

Perceba que os valores para as colunas idade, preco e quantidade são praticamente os mesmos para os dois DataFrames.

As únicas diferenças podem ser encontradas na coluna preco em casas decimais mais avançadas. Porém, no que diz respeito a preço, estamos interessados até a segunda casa decimal, então essa mudança não terá impacto significativo.

Tempo de Processamento – DataFrame Original x DataFrame Otimizado

Para finalizar, vamos executar nosso teste de velocidade de processamento, para ver o ganho obtido.

%timeit df.groupby("categoria_produto")["preco"].mean()
%timeit df.groupby(["categoria_produto", "genero"])["preco"].mean()

Tempo de Processamento DataFrame Original
%timeit df_otimizado.groupby("categoria_produto")["preco"].mean()
%timeit df_otimizado.groupby(["categoria_produto", "genero"])["preco"].mean()

Tempo de Processamento DataFrame Otimizado

Repare que o tempo médio foi de 48.9 milissegundos para 5.33 milissegundos na primeira operação e de 104 milissegundos para 25.7 milissegundos na segunda.

Um ganho de tempo considerável, principalmente se pensarmos que esses tempos vão se acumulando com cada operação realizada e com o tamanho do DataFrame trabalhado.

Conclusão – Como Otimizar DataFrames do Pandas

Nessa aula, mostrei como otimizar DataFrames do Pandas para diminuir o uso de memória e o tempo de processamento, sem perder informações relevantes.

Você aprendeu como alterar cada coluna para o tipo mais adequado e otimizar o tipo de dado armazenado.

Isso representa um ganho significativo no desempenho, melhorando a manipulação e o trabalho com os dados disponíveis.

Hashtag Treinamentos

Para acessar outras publicações de Ciência de Dados, clique aqui!


Quer aprender mais sobre Python com um minicurso básico gratuito?