Otimização de Arquivos Grandes com Pandas
Eu estava lendo algumas notícias no TabNews e achei bem interessante esse post sobre otimização de arquivos com Python: Como otimizei um Arquivo de 1.6 GB para 9 MB usando Python.
Eu entendi a ideia, mas eu queria tentar simular a ideia aplicada pelo autor e verificar a solução proposta por ele.
Todo o código de teste está disponível nesse Notebook.
Construção dos Dataset (CSV)
Para construir o dataset primeiro separei as variáveis comentadas pelo autor. De acordo com ele um trecho dele:
"Após a criação da coluna com as horas, procedi com a agregação da quantidade de passagens por data, hora, tipo de veículo e sentido:"
Desse trecho eu sei que o que ele tinha antes eram dados temporais (coluna com data e hora), tipo de veículo, sentido e quantidade de passagens. Nesse caso, basta eu criar um exemplo tenha todas essas variáveis.
Antes de tudo eu criei algumas variáveis que definem os dados, como tipos de veículo (vehicle_types
), direções (directions
), arquivos de entrada e saída e quandidade de dados (n
).
big_csv_file_name = 'big_out.csv'
small_csv_file_name = 'small_out.csv'
vehicle_types = ['Carro', 'Moto', 'Caminhão', 'Ônibus']
directions = ['Norte', 'Sul', 'Leste', 'Oeste']
n = 22000000
Os códigos abaixo são apenas para construção da base. Nesse caso foi considerado um ano entre o começo e o fim e valores aleatórios de quantidade, entre 1 e 30.
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
start_date = datetime(2023, 1, 1)
end_date = datetime(2023, 12, 31)
date_range = pd.date_range(start_date, end_date, freq='T')
dates = np.random.choice(date_range, size=n)
vehicles = np.random.choice(vehicle_types, size=n)
directions_data = np.random.choice(directions, size=n)
quantity = np.random.randint(1, 30, size=n)
df = pd.DataFrame({
'datetime': dates,
'vehicle_type': vehicles,
'direction': directions_data,
'quantity': quantity
})
Ao fim da criação do dataframe é necessário transformar em csv
para simular o arquivo de input de dados.
df.to_csv(big_csv_file_name, index=False)
Nessa parte eu só vejo o tamanho do arquivo de entrada antes da agragação.
import os
file_size_big = os.path.getsize(big_csv_file_name)
print(f'O arquivo {big_csv_file_name} tem um tamanho de {file_size_big / (1024 * 1024):.2f} MB.')
Aplicação do Agrupamento
Uma vez tenho criado o arquivo de entrada, agora é necessário ler o arquivo de entrada.
import pandas as pd
csv_df = pd.read_csv(big_csv_file_name)
Analisando as informações do dataframe eu notei que da coluna datetime
estava como object
.
csv_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 22000000 entries, 0 to 21999999
Data columns (total 4 columns):
# Column Dtype
--- ------ -----
0 datetime object
1 vehicle_type object
2 direction object
3 quantity int64
dtypes: int64(1), object(3)
memory usage: 671.4+ MB
Isso não é bom de trabalhar, então transformei ela em datetime
. Alterei os outros tipos também.
csv_df['datetime'] = pd.to_datetime(csv_df['datetime'])
csv_df['vehicle_type'] = csv_df['vehicle_type'].astype('category')
csv_df['direction'] = csv_df['direction'].astype('category')
Tendo transformado a coluna datetime
em datetime, então fica fácil ter a data e a hora.
csv_df['hour'] = csv_df['datetime'].dt.hour
csv_df['date'] = csv_df['datetime'].dt.date
Depois é só agregar. Eu tentei agragar de acordo com o que o autor colocou.
aggregated_df = csv_df.groupby(['date', 'hour', 'vehicle_type', 'direction', 'quantity']).size().reset_index(name='count')
Depois de agragado eu transformei para csv
para ter o arquivo de saída reduzido.
aggregated_df.to_csv(small_csv_file_name, index=False)
Aqui eu só faço a mesma coisa que fiz no arquivo de entrada, vendo o tamanho dele.
import os
file_size_small = os.path.getsize(small_csv_file_name)
print(f'O arquivo {big_csv_file_name} tem um tamanho de {file_size_small / (1024 * 1024):.2f} MB.')
Comparando os resultados, dá para notar que houve uma redução significativa. Para 22.000.000 de dados, eu tive uma redução de 84% do arquivo original. Acredito que a media que a base aumenta de tamanho esse resultado deve diminuir, mas já ajuda.
percentage = (file_size_small / file_size_big) * 100
print(f'O arquivo {small_csv_file_name} é {percentage:.2f}% do tamanho do arquivo {big_csv_file_name}.')
Conclusão
Deu para ver que a recomendação do agrupamento foi muito útil e reduziu para um valor bem melhor. Legal que essa estratégia parece ser muito interessante de aplicar para séries temporais, uma vez que eles, geralmente, são dados com muitos ruídos. Outro ponto é que talvez os meus valores não tenha abaixado mais o tamanho do arquivo final devido ao meu range de quantidade de 1 até 30. Isso piora o agrupamento, mas para valores menores de quantidades isso diminui ainda mais o arquivo final.
Existem algumas alternativas para otimizar a quantidade de espaço utilizado para o armazenamento de dados (sem perder a estrutura original). Uma delas é trabalhar com outros formatos de arquivo. O formato CSV que você usa nesse post é popular por ser simples de entender e fácil de um humano visualizar usando um bloco de notas, por exemplo, então é muito utilizado em tutoriais e aulas introdutórias. Porém, para quantidades maiores de dados, e em ambientes profissionais - onde os dados serão armazenados para serem lidos com frequência depois - é mais comum utilizarmos arquivos que utilizam armazenamento do tipo column-wise ao invés de row-wise.
Por exemplo, o formato .parquet è um formato moderno muito otimizado, column-wise, e utilizado com frequência em ambientes de big data (mas pode ser utilizado em ambientes com bases pequenas ou médias também). Ao salvar um arquivo desse tipo, é aplicado um algoritmo de compressão sem perdas nos dados, muito eficiente (é muito maneiro, sugiro procurar depois como funciona). Sugiro testar fazer um benchmark da quantidade de memória utilizada com os dados originais (sem o agrupamento).
No pandas, pra trabalhar com parquets ao invés de csv, é só mudar a chamada da função to_csv() para to_parquet(), então é tão fácil quanto.
Compare a velocidade de leitura deles também! Usando read_parquet() ao invés de read_csv(). como é column-wise, a leitura do arquivo via código é impressionantemente rápida.
A única desvantagem desse tipo de arquivo para casos de análise/ciência de dados é que, como ele é um binário, você não consegue abri-lo com o Bloco de Notas para dar uma olhada em seu conteúdo. Mas como você já está usando o Pandas, é só dar um .head() que essa desvantagem é completamente mitigada. Além disso, abrir um arquivo de 1.6GB no bloco de notas demora anos, né. Melhor usar o head() mesmo.
Muito bom o teste, parabens! No meu artigo acabei esquecendo de colocar os blocos de código, mas a sua estrutura ficou bem parecida com a minha e curiosamente também utilizei dados de tráfego.
Um exemplo que ficou bem didático, para casos de agregação e contagem realmente não precisa todas linhas. Porém quando a linha em si é relevante inidividualmente,realmente pensar em formato parquet.