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:
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])
['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:
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))
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:
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)
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:
print(pd.read_parquet("saida/grande.parquet", columns=["id_pedido", "quantidade"]).shape)
(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:
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()})
{'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:
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)
{"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):
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)
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.
| Formato | Bom para | Perde |
|---|---|---|
| CSV | Trocar com pessoas e outros sistemas | Tipos (datas, categorias, anuláveis) |
| Parquet | Guardar entre etapas, dados grandes | Não abre em editor de texto |
| Excel | Quem trabalha em planilhas | Tipos e velocidade, e tem limite de linhas |
| JSON Lines | Registros de APIs e logs | Compacto e tipos fracos |
| SQL | Dados que já moram em um banco | Depende 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.