Capítulo 32, Avançado
Arquivos grandes: ler em pedaços
Quando um arquivo é maior que a memória, ou grande o bastante para tornar tudo lento, você não precisa lê-lo inteiro. O `chunksize` o entrega em pedaços, e muitas contas se resolvem somando os resultados parciais.
O arquivo
Para ter o que ler, eu replico os pedidos até 400 mil linhas e gravo em CSV. As linhas são repetidas, mas o id_pedido é renumerado, para cada pedido ser único:
import tracemalloc
from pathlib import Path
import numpy as np
import pandas as pd
Path("saida").mkdir(exist_ok=True)
pedidos = pd.read_csv("dados/pedidos.csv")
grande = pd.concat([pedidos] * 500, ignore_index=True)
grande["id_pedido"] = np.arange(1, len(grande) + 1)
grande.to_csv("saida/pedidos_400k.csv", index=False)
print(len(grande), round(Path("saida/pedidos_400k.csv").stat().st_size / 1024 / 1024, 1), "MB")
400000 12.9 MB
Ler em pedaços e combinar os parciais
Com o chunksize=n, o read_csv devolve um iterador: cada volta do laço entrega uma tabela de até n linhas. A estratégia é calcular algo pequeno em cada pedaço e juntar os pequenos no fim. Para uma soma e uma contagem por canal:
completo = pd.read_csv("saida/pedidos_400k.csv")
esperado = completo.groupby("canal")["quantidade"].agg(["sum", "count"])
parciais = []
pedacos = 0
for pedaco in pd.read_csv("saida/pedidos_400k.csv", usecols=["canal", "quantidade"], chunksize=50_000):
pedacos += 1
parciais.append(pedaco.groupby("canal")["quantidade"].agg(["sum", "count"]))
total = pd.concat(parciais).groupby(level=0).sum()
print(pedacos)
pd.testing.assert_frame_equal(esperado, total)
print((total["sum"] / total["count"]).round(3).to_dict())
8
{'app': 5.246, 'loja': 5.488, 'site': 5.528}
Foram 8 pedaços, e o resultado é idêntico ao de ler tudo de uma vez (o assert_frame_equal não reclamou). Repare que a média não foi calculada em cada pedaço: guardei a soma e a contagem, e dividi no fim. A média das médias de pedaços de tamanhos diferentes não é a média geral.
O ganho: memória
O pico de memória do laço é o de um pedaço, e não o do arquivo inteiro. O tracemalloc mede o pico de cada abordagem:
def pico_de_memoria(funcao):
tracemalloc.start()
funcao()
_, pico = tracemalloc.get_traced_memory()
tracemalloc.stop()
return pico
def somar_em_pedacos():
for pedaco in pd.read_csv("saida/pedidos_400k.csv", usecols=["canal", "quantidade"], chunksize=50_000):
pedaco.groupby("canal")["quantidade"].sum()
pico_tudo = pico_de_memoria(lambda: pd.read_csv("saida/pedidos_400k.csv"))
pico_pedacos = pico_de_memoria(somar_em_pedacos)
print("o pico em pedaços é menos da metade:", pico_pedacos < pico_tudo / 2)
o pico em pedaços é menos da metade: True
Duas outras ferramentas ajudam a olhar um arquivo grande sem pagar por ele: o nrows lê só as primeiras linhas (para inspecionar o formato) e o usecols lê só as colunas que você precisa (como no capítulo 29).
O que se resolve em pedaços, e o que não
Nem toda conta se divide. Uma conta é decomponível quando o resultado geral sai dos resultados parciais:
| Conta | Em pedaços | Guardar de cada pedaço |
|---|---|---|
| Soma, contagem, mínimo, máximo | Sim | A própria soma, contagem, mínimo, máximo |
| Média | Sim | A soma e a contagem |
| Desvio padrão | Sim, com cuidado | Contagem, soma e soma dos quadrados |
| Mediana e percentis | Não diretamente | Exige todos os dados (ou um método aproximado) |
Valores distintos (nunique) | Não somando | Exige o conjunto dos valores vistos |
O último caso engana: somar as contagens de valores distintos de cada pedaço conta duas vezes o mesmo cliente que aparece em pedaços diferentes:
verdadeiro = completo.groupby("canal")["id_cliente"].nunique()
parciais_unicos = []
for pedaco in pd.read_csv("saida/pedidos_400k.csv", usecols=["canal", "id_cliente"], chunksize=50_000):
parciais_unicos.append(pedaco.groupby("canal")["id_cliente"].nunique())
somados = pd.concat(parciais_unicos).groupby(level=0).sum()
print(verdadeiro.to_dict())
print(bool((somados > verdadeiro).all()))
{'app': 58, 'loja': 64, 'site': 64}
True
Há entre 58 e 64 clientes distintos em cada canal (a primeira linha do resultado), e a soma dos parciais deu bem mais, porque o mesmo cliente é recontado a cada pedaço. A solução correta guarda o conjunto dos clientes vistos (set) e une os conjuntos no fim. E, quando os dados ficam grandes demais até para isso, vale considerar ferramentas pensadas para dados que não cabem na memória, como o DuckDB ou o Polars, que este curso não cobre.
Exercício 1
A média por canal, em pedaços
Escreva media_por_canal_em_pedacos(caminho, tamanho), que calcule a média da quantidade por canal lendo o arquivo em pedaços (guardando soma e contagem), e confira que o resultado é igual ao da leitura completa.