Streamlit transforma scripts Python em aplicações de dados com widgets e visualizações. Ele é útil para protótipos, ferramentas internas e relatórios exploráveis, mas continua exigindo validação de entrada, controle de acesso e tratamento de desempenho.

Neste exemplo, criaremos um painel de vendas a partir de CSV. Para revisar a manipulação dos dados, consulte o guia de pandas em Python.

Instalar e executar

python -m pip install streamlit pandas
streamlit run app.py

Crie app.py:

import pandas as pd
import streamlit as st

st.set_page_config(page_title="Vendas", layout="wide")
st.title("Painel de vendas")

arquivo = st.file_uploader("Envie um CSV", type=["csv"])
if arquivo is None:
    st.info("Envie um arquivo para iniciar.")
    st.stop()

df = pd.read_csv(arquivo)
colunas = {"data", "categoria", "valor"}
if not colunas.issubset(df.columns):
    st.error("O CSV precisa conter data, categoria e valor.")
    st.stop()

df["data"] = pd.to_datetime(df["data"], errors="coerce")
df["valor"] = pd.to_numeric(df["valor"], errors="coerce")
df = df.dropna(subset=["data", "valor"])

Validar colunas e tipos evita que uma entrada ruim produza um gráfico enganoso ou uma exceção pouco clara.

Filtros, métricas e gráfico

opcoes = sorted(df["categoria"].dropna().unique())
selecionadas = st.multiselect("Categorias", opcoes, default=opcoes)
filtrado = df[df["categoria"].isin(selecionadas)]

total = filtrado["valor"].sum()
media = filtrado["valor"].mean()

c1, c2 = st.columns(2)
c1.metric("Receita", f"R$ {total:,.2f}")
c2.metric("Venda média", f"R$ {media:,.2f}")

por_dia = filtrado.groupby("data", as_index=False)["valor"].sum()
st.line_chart(por_dia, x="data", y="valor")
st.dataframe(filtrado, use_container_width=True)

O modelo de execução do Streamlit reexecuta o script após interações. A documentação de conceitos explica esse fluxo.

Cache com intenção

Use st.cache_data para funções que retornam dados serializáveis e podem ser recalculadas sem efeitos colaterais:

@st.cache_data(ttl=600)
def carregar_csv(caminho: str) -> pd.DataFrame:
    return pd.read_csv(caminho)

Defina TTL quando a origem muda. Não coloque informações de um usuário em cache compartilhado sem compreender o isolamento. Para conexões e recursos, consulte st.cache_resource na documentação oficial de cache.

Normalizar os dados antes de exibir

O painel deve ter um único fluxo de transformação, venha a fonte de upload, banco ou API. Separe regras de dados dos widgets:

def preparar_vendas(bruto: pd.DataFrame) -> pd.DataFrame:
    obrigatorias = {"data", "categoria", "valor"}
    ausentes = obrigatorias.difference(bruto.columns)
    if ausentes:
        raise ValueError(f"Colunas ausentes: {', '.join(sorted(ausentes))}")
    limpo = bruto.copy()
    limpo["data"] = pd.to_datetime(limpo["data"], errors="coerce")
    limpo["valor"] = pd.to_numeric(limpo["valor"], errors="coerce")
    limpo["categoria"] = limpo["categoria"].astype("string").str.strip()
    return limpo.dropna(subset=["data", "categoria", "valor"])

try:
    df = preparar_vendas(pd.read_csv(arquivo))
except (ValueError, pd.errors.ParserError, UnicodeDecodeError) as erro:
    st.error(f"Não foi possível processar o arquivo: {erro}")
    st.stop()

A cópia evita mutações inesperadas. Em produção, informe quantas linhas foram rejeitadas. Descartar silenciosamente metade do arquivo pode gerar um gráfico bonito e incorreto. Moeda, fuso horário, separador decimal e tratamento de estornos precisam ser regras explícitas.

Para uploads grandes, limite o tamanho antes da leitura e carregue apenas colunas necessárias. A extensão não garante conteúdo seguro. Nunca envie conteúdo recebido para comandos do sistema nem desserialize pickle não confiável.

Criar filtros que tratam resultado vazio

Datas e categorias devem partir do DataFrame validado:

inicio = df["data"].min().date()
fim = df["data"].max().date()
periodo = st.date_input("Período", value=(inicio, fim), min_value=inicio, max_value=fim)

if len(periodo) == 2:
    data_inicial, data_final = periodo
    mascara = df["data"].dt.date.between(data_inicial, data_final)
    filtrado = df.loc[mascara & df["categoria"].isin(selecionadas)].copy()
else:
    filtrado = df.iloc[0:0].copy()

if filtrado.empty:
    st.warning("Nenhuma venda corresponde aos filtros.")
    st.stop()

Parar evita média NaN e gráfico vazio sem explicação. st.session_state pode preservar escolhas nas reexecuções, mas não é armazenamento durável e não substitui banco.

Definir o significado das métricas

“Receita” pode significar venda bruta, líquida, valor reconhecido ou caixa recebido. Declare a definição perto do número. Compare períodos equivalentes ao exibir variação.

total = filtrado["valor"].sum()
quantidade = len(filtrado)
media = total / quantidade if quantidade else 0
c1, c2, c3 = st.columns(3)
c1.metric("Receita", f"R$ {total:,.2f}")
c2.metric("Linhas", f"{quantidade:,}")
c3.metric("Valor médio", f"R$ {media:,.2f}")

Se cada linha for item e não pedido, chamar a contagem de “pedidos” exagera transações. Agregue datas na granularidade pretendida; dados diários podem ficar ruidosos em um ano.

Colocar o cache no limite correto

Cache funciona ao redor de trabalho caro, determinístico e sem efeitos colaterais:

from io import BytesIO

@st.cache_data(ttl=600, show_spinner="Lendo dados...")
def ler_vendas(conteudo: bytes) -> pd.DataFrame:
    return preparar_vendas(pd.read_csv(BytesIO(conteudo)))

df = ler_vendas(arquivo.getvalue())

st.cache_data serve para consultas e transformações. st.cache_resource serve para modelo ou conexão compartilhada. Não armazene em cache funções que enviam e-mail ou registram pagamento: um acerto no cache pularia a ação. Defina TTL quando atualização importa.

Organizar, testar e publicar

Mantenha transformações puras em vendas.py e chamadas ao Streamlit em app.py. Assim, regras são testadas sem navegador:

def test_preparar_vendas_remove_valores_invalidos():
    bruto = pd.DataFrame({
        "data": ["2026-01-02", "inválida"],
        "categoria": ["Livros", "Livros"],
        "valor": ["12.50", "desconhecido"],
    })
    resultado = preparar_vendas(bruto)
    assert len(resultado) == 1
    assert resultado.iloc[0]["valor"] == 12.5

Teste colunas ausentes, arquivo vazio, duplicatas, negativos, limites de data e encodings incomuns. A referência oficial de testes mostra fluxos com widgets.

Fixe dependências e execute em ambiente limpo. Configure segredos na plataforma, nunca no Git. Se usuários enxergam linhas diferentes, aplique autorização ao buscar dados, em vez de baixar tudo e filtrar somente na interface. Logs devem ajudar sem registrar arquivos, tokens ou dados pessoais. Defina limite de arquivo, timeout, validade do cache e responsável pelas métricas.

Antes de publicar

  • Fixe dependências e teste com arquivos pequenos, vazios e inválidos.
  • Mantenha segredos fora do repositório.
  • Limite tamanho de upload e consumo de memória.
  • Explique unidades, filtros e origem dos dados.
  • Use autenticação para dados internos.
  • Separe carregamento, transformação e interface em funções testáveis.

Streamlit reduz o trabalho de interface, não o trabalho de engenharia dos dados. Um painel confiável deixa claro o que foi filtrado, como as métricas foram calculadas e quais limitações existem.

Diagnosticar problemas antes de adicionar recursos

Se um widget perde o valor, verifique se sua chave ou suas opções mudam durante a reexecução. Uma categoria selecionada pode deixar de existir depois de uma normalização diferente. Chaves estáveis e uma regra única evitam reinicializações inesperadas. Se uma consulta cara roda a cada clique, confira se a função em cache recebe argumentos estáveis e não depende de valor global omitido.

Quando totais divergem de outro relatório, compare a origem, o instante do snapshot, o fuso horário, a inclusão das datas-limite, a política de duplicatas, os filtros e a granularidade. Documente essas escolhas junto à métrica antes de alterar a formatação. Para reduzir memória, agregue na origem, pagine tabelas detalhadas, selecione apenas colunas necessárias e meça com arquivo representativo.

Um erro de parsing deve explicar a ação possível: conferir colunas, encoding ou separador. Não mostre stack trace ao leitor, mas preserve detalhes seguros no log. Timeout precisa permitir nova tentativa sem duplicar efeitos, embora um painel de leitura não deva executar efeitos durante consulta.

Evoluir as métricas com responsabilidade

Comece por uma pergunta decisória, não por uma coleção de gráficos. Cada visual deve responder algo e informar período, unidade e filtros ativos. Ao adicionar banco ou API, adapte o resultado ao mesmo esquema validado usado no CSV. Isso evita regras paralelas que produzem números diferentes.

Versione mudanças na definição das métricas. Se “receita” passar a descontar estornos, usuários precisam saber a partir de quando a comparação mudou. Mostre o horário da última atualização e qualquer atraso conhecido. Não apresente casas decimais que a fonte não sustenta.

Se dados antigos permanecerem disponíveis durante indisponibilidade, identifique-os como desatualizados. Decida quanto tempo podem ser usados e quando é melhor interromper a exibição. Nunca faça uma cópia velha parecer atual.

Melhorar usabilidade e acessibilidade

Rótulos precisam descrever o filtro sem depender de instrução externa. Não comunique resultado somente por cor; inclua texto, símbolo acessível ou valor. Mantenha uma tabela quando o gráfico representa valores que precisam ser consultados com exatidão. Explique siglas e métricas específicas do domínio.

Teste navegação por teclado, foco, contraste e telas estreitas. Um layout wide não elimina a necessidade de verificar celular. Tabelas com muitas colunas podem exigir seleção de campos prioritários em vez de rolagem infinita.

Evite gráficos tridimensionais e escalas truncadas que exagerem diferenças. Ordene categorias de modo significativo. Se houver poucos pontos, mostre valores; se houver milhares, agregue e permita detalhamento progressivo.

Operar o painel

Observe duração de carregamento, falhas de consulta, rejeição de arquivos e uso de memória sem registrar conteúdo sensível. Defina limites e alertas proporcionais. Uma pessoa deve ser responsável pela origem e outra, ou a mesma de forma explícita, pela definição da métrica.

Faça revisão com alguém que conheça o domínio antes de liberar. Engenharia garante cálculo reproduzível, enquanto a pessoa especialista confirma se o cálculo representa a pergunta correta. Registre limitações conhecidas na própria página.

Tenha um caminho para voltar a uma versão anterior do código e preserve compatibilidade com o esquema dos dados. Mudanças de coluna devem falhar com mensagem clara, não produzir zero silencioso. Uma pequena verificação automática da fonte antes do deploy evita publicar uma interface que inicia, mas não consegue ler dados reais.

Por fim, revise o painel como leitor: o período está visível, os filtros podem ser limpos, o estado vazio faz sentido, a atualização é conhecida e a origem é identificada? Essas respostas determinam confiança mais do que a quantidade de componentes na tela.