Pular para o conteúdo

    Capítulo 26, Intermediário

    Ler e gravar dados

    Um resultado que só existe na memória some quando o programa termina. Aqui entram os formatos que importam (CSV, Parquet, Excel, SQL), e o que cada um perde pelo caminho.

    CSV, com os parâmetros certos

    O to_csv tem os mesmos ajustes da leitura. Para quem vai abrir o arquivo no Excel em português, sep=";" e decimal="," evitam números virando texto, e o date_format fixa a escrita das datas:

    intermediario/cap26_ler_gravar.pylinhas 10 a 22
    from pathlib import Path
    
    import numpy as np
    import pandas as pd
    
    pd.set_option("display.width", 170)
    pd.set_option("display.max_columns", 20)
    Path("saida").mkdir(exist_ok=True)
    
    pedidos = pd.read_csv("dados/pedidos.csv", parse_dates=["data_pedido"])
    amostra = pedidos.head(3)
    amostra.to_csv("saida/br.csv", index=False, sep=";", decimal=",", date_format="%d/%m/%Y")
    print(Path("saida/br.csv").read_text(encoding="utf-8").splitlines()[:2])
    
    Saída
    ['id_pedido;id_cliente;id_produto;quantidade;data_pedido;desconto;canal', '1;57;17;6;01/01/2025;0,0;site']
    

    O problema do CSV é que ele é só texto: tudo o que não é número simples perde o tipo na volta. Dá para ver exatamente o que se perde:

    intermediario/cap26_ler_gravar.pylinhas 24 a 31
    tipado = pedidos.assign(
        canal=pedidos["canal"].astype("category"),
        id_cliente=pedidos["id_cliente"].astype("Int64"),
    )
    tipado.to_csv("saida/tipado.csv", index=False)
    de_volta = pd.read_csv("saida/tipado.csv")
    for coluna in ("data_pedido", "canal", "id_cliente"):
        print(coluna, str(tipado[coluna].dtype), "->", str(de_volta[coluna].dtype))
    
    Saída
    data_pedido datetime64[us] -> str
    canal category -> str
    id_cliente Int64 -> int64
    

    A data voltou como texto, a categoria voltou como texto, e o inteiro anulável voltou como int64. Quem lê o CSV precisa refazer todas as conversões (e precisa saber quais).

    Parquet: o formato que lembra os tipos

    O Parquet é um formato binário, em colunas, feito para análise. Ele guarda os tipos (datas, categorias, anuláveis) e comprime. Em arquivos pequenos, os metadados pesam e ele não ganha em tamanho, mas, em dados de verdade, a diferença é grande:

    intermediario/cap26_ler_gravar.pylinhas 36 a 45
    tipado.to_parquet("saida/tipado.parquet", index=False)
    voltou = pd.read_parquet("saida/tipado.parquet")
    print(voltou.dtypes.astype(str).to_dict() == tipado.dtypes.astype(str).to_dict())
    
    grande = pd.concat([tipado] * 100, ignore_index=True)
    grande.to_csv("saida/grande.csv", index=False)
    grande.to_parquet("saida/grande.parquet", index=False)
    csv_kb = Path("saida/grande.csv").stat().st_size / 1024
    parquet_kb = Path("saida/grande.parquet").stat().st_size / 1024
    print(len(grande), "o parquet é menos de um quarto do csv:", parquet_kb < csv_kb / 4)
    
    Saída
    True
    80000 o parquet é menos de um quarto do csv: True
    

    Como o Parquet é em colunas, dá para ler só algumas, o que em um arquivo grande economiza tempo e memória:

    intermediario/cap26_ler_gravar.pylinha 47
    print(pd.read_parquet("saida/grande.parquet", columns=["id_pedido", "quantidade"]).shape)
    
    Saída
    (80000, 2)
    

    O Parquet precisa do pyarrow, que está nas dependências deste curso. A minha regra: CSV para trocar dados com gente (planilhas, outros sistemas), Parquet para guardar dados entre etapas de uma análise.

    Excel

    O Excel exige o openpyxl. Várias tabelas podem ir para abas de um mesmo arquivo, com o ExcelWriter, e a leitura de todas as abas devolve um dicionário:

    intermediario/cap26_ler_gravar.pylinhas 52 a 57
    with pd.ExcelWriter("saida/relatorio.xlsx") as escritor:
        pedidos.head(3).to_excel(escritor, sheet_name="primeiros", index=False)
        pedidos.tail(2).to_excel(escritor, sheet_name="ultimos", index=False)
    
    abas = pd.read_excel("saida/relatorio.xlsx", sheet_name=None)
    print({nome: tabela.shape for nome, tabela in abas.items()})
    
    Saída
    {'primeiros': (3, 7), 'ultimos': (2, 7)}
    

    JSON por linhas

    Para registros que chegam de APIs ou logs, o formato JSON Lines (um objeto por linha) é o mais prático, e dá para ler aos poucos:

    intermediario/cap26_ler_gravar.pylinhas 62 a 64
    amostra.to_json("saida/pedidos.jsonl", orient="records", lines=True, date_format="iso")
    print(Path("saida/pedidos.jsonl").read_text(encoding="utf-8").splitlines()[0][:60])
    print(pd.read_json("saida/pedidos.jsonl", lines=True).shape)
    
    Saída
    {"id_pedido":1,"id_cliente":57,"id_produto":17,"quantidade":
    (3, 7)
    

    SQL

    O Pandas fala com bancos de dados. O sqlite3 já vem com o Python. O to_sql grava uma tabela, e o read_sql lê o resultado de uma consulta. Para passar valores, use parâmetros (?), e nunca monte o SQL com texto (a injeção do capítulo 15 do curso de FastAPI vale aqui também):

    intermediario/cap26_ler_gravar.pylinhas 69 a 80
    import sqlite3
    
    conexao = sqlite3.connect(":memory:")
    pedidos.to_sql("pedidos", conexao, index=False, if_exists="replace")
    
    total = pd.read_sql("SELECT COUNT(*) AS n FROM pedidos", conexao)["n"].iloc[0]
    caros = pd.read_sql(
        "SELECT id_pedido, quantidade FROM pedidos WHERE quantidade >= ? ORDER BY quantidade DESC LIMIT 3",
        conexao,
        params=(10,),
    )
    print(int(total), caros.shape)
    
    Saída
    800 (3, 2)
    

    Uma consulta SQL é, muitas vezes, a forma certa de reduzir os dados antes de trazê-los: filtrar e agregar no banco, e só carregar o resultado, em vez de trazer tudo e filtrar no Pandas.

    FormatoBom paraPerde
    CSVTrocar com pessoas e outros sistemasTipos (datas, categorias, anuláveis)
    ParquetGuardar entre etapas, dados grandesNão abre em editor de texto
    ExcelQuem trabalha em planilhasTipos e velocidade, e tem limite de linhas
    JSON LinesRegistros de APIs e logsCompacto e tipos fracos
    SQLDados que já moram em um bancoDepende de uma conexão

    Exercício 1

    Ida e volta sem perder nada

    Escreva ida_e_volta_parquet(tabela, caminho), que grave uma tabela em Parquet, leia de volta e devolva True se os tipos e os valores forem idênticos. Teste com a tabela tipado.