Streamlit convierte scripts Python en aplicaciones de datos interactivas. Es útil para prototipos e informes, pero todavía exige validación, control de acceso y cuidado con el rendimiento.
python -m pip install streamlit pandas
streamlit run app.py
Cargar y validar
import pandas as pd
import streamlit as st
st.title("Panel de ventas")
archivo = st.file_uploader("Sube un CSV", type=["csv"])
if archivo is None:
st.info("Sube un archivo para comenzar.")
st.stop()
df = pd.read_csv(archivo)
requeridas = {"fecha", "categoria", "valor"}
if not requeridas.issubset(df.columns):
st.error("Faltan columnas requeridas.")
st.stop()
Valida tipos antes de calcular. El contenido de ciencia de datos con Python amplía el flujo.
opciones = sorted(df["categoria"].dropna().unique())
seleccion = st.multiselect("Categorías", opciones, default=opciones)
filtrado = df[df["categoria"].isin(seleccion)]
st.metric("Ingresos", f"${filtrado['valor'].sum():,.2f}")
st.line_chart(filtrado, x="fecha", y="valor")
Streamlit reejecuta el script tras interacciones. La guía oficial de conceptos explica el modelo.
Usa @st.cache_data(ttl=600) para cargas repetibles, sin mezclar datos sensibles de usuarios. Consulta la documentación de caché.
Normalizar antes de visualizar
Separa la transformación de los widgets para que un mismo flujo sirva para archivos, bases de datos o APIs:
def preparar_ventas(datos: pd.DataFrame) -> pd.DataFrame:
requeridas = {"fecha", "categoria", "valor"}
ausentes = requeridas.difference(datos.columns)
if ausentes:
raise ValueError(f"Faltan columnas: {', '.join(sorted(ausentes))}")
limpio = datos.copy()
limpio["fecha"] = pd.to_datetime(limpio["fecha"], errors="coerce")
limpio["valor"] = pd.to_numeric(limpio["valor"], errors="coerce")
limpio["categoria"] = limpio["categoria"].astype("string").str.strip()
return limpio.dropna(subset=["fecha", "categoria", "valor"])
try:
df = preparar_ventas(pd.read_csv(archivo))
except (ValueError, pd.errors.ParserError, UnicodeDecodeError) as error:
st.error(f"No fue posible procesar el archivo: {error}")
st.stop()
La copia evita mutaciones inesperadas. Informa cuántas filas se descartaron: eliminar silenciosamente media carga puede producir un gráfico convincente pero falso. Moneda, zona horaria, separador decimal y reembolsos deben tener reglas explícitas.
Limita el tamaño antes de leer cargas grandes. Una extensión no garantiza contenido seguro. Nunca envíes contenido cargado a comandos del sistema ni deserialices pickle de origen desconocido.
Crear filtros que admitan resultados vacíos
inicio = df["fecha"].min().date()
fin = df["fecha"].max().date()
periodo = st.date_input("Periodo", value=(inicio, fin), min_value=inicio, max_value=fin)
if len(periodo) == 2:
desde, hasta = periodo
mascara = df["fecha"].dt.date.between(desde, hasta)
filtrado = df.loc[mascara & df["categoria"].isin(seleccion)].copy()
else:
filtrado = df.iloc[0:0].copy()
if filtrado.empty:
st.warning("No hay ventas para estos filtros.")
st.stop()
Detener la ejecución evita promedios NaN y gráficos vacíos sin explicación. st.session_state conserva selecciones entre ejecuciones, pero no es almacenamiento duradero.
Definir las métricas
“Ingresos” puede significar ventas brutas, netas, reconocidas o cobros. Explica la definición junto al valor y compara periodos equivalentes.
total = filtrado["valor"].sum()
cantidad = len(filtrado)
promedio = total / cantidad if cantidad else 0
c1, c2, c3 = st.columns(3)
c1.metric("Ingresos", f"${total:,.2f}")
c2.metric("Filas", f"{cantidad:,}")
c3.metric("Valor medio", f"${promedio:,.2f}")
Si una fila representa un producto y no un pedido, llamarla “pedido” exagera transacciones. Agrupa fechas con la granularidad adecuada; un año de datos diarios puede requerir semanas o meses.
Ubicar bien la caché
La caché debe envolver trabajo costoso, determinista y sin efectos secundarios:
from io import BytesIO
@st.cache_data(ttl=600, show_spinner="Leyendo datos...")
def leer_ventas(contenido: bytes) -> pd.DataFrame:
return preparar_ventas(pd.read_csv(BytesIO(contenido)))
df = leer_ventas(archivo.getvalue())
st.cache_data sirve para consultas y transformaciones; st.cache_resource, para modelos o conexiones compartidas. No almacenes funciones que envían correo o registran pagos, porque un acierto omitiría la acción. Usa TTL cuando la frescura importa.
Organizar, probar y desplegar
Mantén transformaciones puras en ventas.py y llamadas Streamlit en app.py:
def test_preparar_ventas_elimina_valores_invalidos():
datos = pd.DataFrame({
"fecha": ["2026-01-02", "incorrecta"],
"categoria": ["Libros", "Libros"],
"valor": ["12.50", "desconocido"],
})
resultado = preparar_ventas(datos)
assert len(resultado) == 1
assert resultado.iloc[0]["valor"] == 12.5
Prueba columnas ausentes, archivos vacíos, duplicados, negativos, límites de fecha y codificaciones. La referencia oficial de pruebas cubre flujos con widgets.
Fija dependencias y ejecuta en un entorno limpio. Guarda secretos en la plataforma, nunca en Git. Si usuarios ven filas distintas, autoriza al consultar, no después de descargar todo. Los logs no deben guardar archivos, tokens ni datos personales. Define límites de carga, timeout, vigencia de caché y un responsable de cada métrica.
Antes de publicar, fija dependencias, limita uploads, prueba archivos inválidos, protege secretos y exige autenticación para datos internos. Streamlit reduce código de interfaz, no la responsabilidad de explicar fuentes y métricas.
Diagnosticar antes de añadir funciones
Si un widget pierde su valor, comprueba si su clave o sus opciones cambian durante la nueva ejecución. Una categoría seleccionada puede desaparecer después de otra normalización. Claves estables y una regla única evitan reinicios inesperados. Si una consulta costosa se repite con cada clic, revisa los argumentos de la función almacenada y cualquier valor global.
Cuando un total difiere de otro informe, compara fuente, instante del snapshot, zona horaria, inclusión de fechas límite, duplicados, filtros y granularidad. Documenta esas decisiones junto a la métrica antes de cambiar el formato. Para reducir memoria, agrega en el origen, pagina tablas, elige columnas necesarias y mide con archivos representativos.
Un error de parsing debe indicar una acción: revisar columnas, codificación o separador. No muestres el stack trace al lector, pero conserva detalles seguros en logs. Un timeout debe permitir reintentar sin duplicar efectos.
Evolucionar las métricas
Comienza con una pregunta decisoria, no con una colección de gráficos. Cada visual debe responder algo e indicar periodo, unidad y filtros activos. Cuando añadas una base o API, adapta su resultado al mismo esquema validado del CSV para evitar reglas paralelas.
Versiona cambios de definición. Si “ingresos” pasa a descontar reembolsos, los usuarios deben saber desde cuándo cambió la comparación. Muestra la última actualización y cualquier retraso. No presentes más decimales de los que la fuente justifica.
Si conservas datos anteriores durante una caída, identifícalos como desactualizados. Decide cuánto tiempo pueden utilizarse y cuándo conviene detener la página. Una copia antigua nunca debe parecer actual.
Cuidar usabilidad y accesibilidad
Las etiquetas deben describir filtros sin instrucciones externas. No comuniques estado solo con color; incluye texto o valor. Conserva tabla cuando un gráfico representa cifras que deben consultarse con exactitud. Explica siglas y métricas del dominio.
Prueba teclado, foco, contraste y pantallas estrechas. Un layout ancho no sustituye la revisión móvil. Muchas columnas pueden requerir una selección prioritaria en lugar de desplazamiento interminable.
Evita gráficos tridimensionales y escalas truncadas que exageran diferencias. Ordena categorías con un criterio significativo. Con pocos puntos, muestra valores; con miles, agrega y permite explorar detalles.
Operar el dashboard
Observa duración, fallos de consulta, archivos rechazados y memoria sin registrar contenido sensible. Define límites y alertas proporcionales. Debe existir una persona responsable del origen y de la definición de cada métrica.
Revisa con alguien experto en el dominio. Ingeniería asegura un cálculo reproducible; conocimiento del negocio confirma que responde la pregunta correcta. Registra limitaciones conocidas en la página.
Mantén una forma de volver a la versión anterior y conserva compatibilidad con el esquema. Una columna ausente debe producir mensaje claro, no un cero silencioso. Una verificación automática con datos representativos antes del despliegue detecta interfaces que arrancan pero no leen la fuente real.
Finalmente, revisa como lector: ¿el periodo está visible, los filtros pueden limpiarse, el estado vacío se entiende, la actualización es conocida y la fuente aparece? Esas respuestas crean más confianza que la cantidad de componentes.
Antes de liberar una versión, recorre la aplicación con un archivo representativo y otro malformado. Confirma que los filtros se pueden limpiar, que el periodo elegido permanece visible y que una descarga contiene los mismos registros filtrados de la pantalla. Compara manualmente al menos un total con la fuente.
Prueba también ausencia de categorías, fechas fuera de orden, reembolsos y un archivo que solo tenga encabezados. El estado vacío no es una excepción rara, sino una parte normal de una interfaz filtrable. Debe explicar cómo volver a obtener resultados.
No mezcles adquisición, transformación y presentación en una función enorme. Las fronteras pequeñas permiten cambiar un gráfico sin alterar la lectura y cambiar la fuente sin reescribir los widgets. Ese diseño reduce errores y hace posible revisar cada regla por separado.