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:
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.
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()
df.info()
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.
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()
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")
Com isso, temos uma descrição inicial dos nossos dados.
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()
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()
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.
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()
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.
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()
Perceba que temos a mesma estrutura, quantidade de dados, tipos e uso de memória que tínhamos no DataFrame original.
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()
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
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()
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.
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.
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.
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.
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.
É 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
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()
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()
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()
df_otimizado.describe()
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.
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()
%timeit df_otimizado.groupby("categoria_produto")["preco"].mean()
%timeit df_otimizado.groupby(["categoria_produto", "genero"])["preco"].mean()
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.
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.
Para acessar outras publicações de Ciência de Dados, clique aqui!
Expert em conteúdos da Hashtag Treinamentos. Auxilia na criação de conteúdos de variados temas voltados para aqueles que acompanham nossos canais.