Pular para o conteúdo

    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:

    avancado/cap32_arquivos_grandes.pylinhas 10 a 21
    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")
    
    Saída
    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:

    avancado/cap32_arquivos_grandes.pylinhas 26 a 38
    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())
    
    Saída
    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:

    avancado/cap32_arquivos_grandes.pylinhas 43 a 58
    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)
    
    Saída
    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:

    ContaEm pedaçosGuardar de cada pedaço
    Soma, contagem, mínimo, máximoSimA própria soma, contagem, mínimo, máximo
    MédiaSimA soma e a contagem
    Desvio padrãoSim, com cuidadoContagem, soma e soma dos quadrados
    Mediana e percentisNão diretamenteExige todos os dados (ou um método aproximado)
    Valores distintos (nunique)Não somandoExige 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:

    avancado/cap32_arquivos_grandes.pylinhas 63 a 69
    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()))
    
    Saída
    {'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.