Skip to content

    Chapter 32, Advanced

    Large files: read in chunks

    When a file is larger than memory, or large enough to slow everything down, you don't need to read it all at once. The `chunksize` delivers it in chunks, and many calculations can be resolved by summing the partial results.

    The file

    To have something to read, I replicate the orders up to 400 thousand lines and save in CSV. The lines are repeated, but the id_pedido is renumbered, so each order is unique:

    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")
    
    Output
    400000 12.9 MB
    

    Read in chunks and combine the partials

    With the chunksize=n, the read_csv returns a iterator: each loop iteration delivers a table of up to n lines. The strategy is to calculate something small in each chunk and join the small results at the end. For a sum and a count by channel:

    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())
    
    Output
    8
    {'app': 5.246, 'loja': 5.488, 'site': 5.528}
    

    There were 8 chunks, and the result is identical compared to reading everything at once (the assert_frame_equal did not complain). Note that the average are not was calculated in each chunk: I saved the sum and the count, and I divided it at the end. The average of the averages of pieces of different sizes are not is the overall average.

    The gain: memory

    The peak memory of the loop is that of one piece, and not that of the entire file. The tracemalloc measures the peak of each approach:

    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)
    
    Output
    o pico em pedaços é menos da metade: True
    

    Two other tools help to look at a large file without paying for it: the nrows reads only the first lines (to inspect the format) and the usecols reads only the columns you need (as in chapter 29).

    What can be solved in chunks, and what cannot

    Not every calculation can be divided. A calculation is decomposable when the overall result comes from the partial results:

    CalculationIn piecesStore from each piece
    Sum, count, minimum, maximumYesThe sum itself, count, minimum, maximum
    AverageYesThe sum and the count
    Standard deviationYes, with careCount, sum, and sum of squares
    Median and percentilesNo directlyRequires all data (or an approximate method)
    Distinct values (nunique)No summingRequires the set of seen values

    The last case is misleading: summing the counts of distinct values from each piece counts the same customer appearing in different pieces twice:

    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()))
    
    Output
    {'app': 58, 'loja': 64, 'site': 64}
    True
    

    There are between 58 and 64 distinct clients in each channel (the first line of the result), and the sum of the partials gave much more, because the same client is counted again for each piece. The correct solution keeps the set of clients seen (set) and merges the sets at the end. And, when the data gets too large even for that, it is worth considering tools designed for data that doesn't fit in memory, like DuckDB or Polars, which this course does not cover.

    Exercise 1

    The average per channel, in chunks

    Write media_por_canal_em_pedacos(caminho, tamanho), which calculates the average of the quantidade per channel by reading the file in chunks (keeping sum and count), and check that the result is equal to that of the complete read.