¿Por qué Streamlit?
Hay muchas herramientas para construir dashboards en Python y R. Un resumen rápido:
| Herramienta | Lenguaje | Ideal para | Curva de aprendizaje |
|---|---|---|---|
| Streamlit | Python | Apps de datos rápidas, prototipos y demos | Muy baja |
| Dash (Plotly) | Python | Dashboards empresariales con callbacks finos | Media |
| Bokeh | Python | Gráficos interactivos y streaming de datos | Media |
| Seaborn | Python | Gráficos estadísticos estáticos (reportes) | Baja |
| Shiny | R / Python | Apps reactivas, muy usado en estadística | Media |
Elegí Streamlit porque un script de Python normal se convierte en una app web, sin HTML, CSS ni JavaScript. Cada vez que el usuario mueve un filtro, Streamlit vuelve a ejecutar el script de arriba hacia abajo. Ese modelo mental es muy simple.
Arquitectura del proyecto
. ├── app.py # Interfaz de Streamlit + gráficos ├── src/data.py # Lógica de datos (sin Streamlit → testeable) ├── data/sales.csv # Dataset sintético (6 000 pedidos 2024-2025) ├── scripts/generate_data.py # Generador reproducible del dataset ├── tests/ # Tests unitarios + smoke test de la app ├── .streamlit/config.toml # Tema visual ├── .github/workflows/ci-cd.yml ├── requirements.txt └── requirements-dev.txt La decisión de diseño más importante es separar la lógica (src/data.py) de la interfaz (app.py). Así puedo probar los cálculos con pytest sin levantar un navegador.
Paso 1: Los datos
Para no depender de datos privados, generé un dataset sintético pero realista con numpy y una semilla fija (seed=42), así siempre sale igual. Incluye:
- 5 regiones del Perú (Lima, Arequipa, Trujillo, Cusco, Piura)
- 5 categorías (Electronics, Home, Sports, Clothing, Beauty)
- 2 canales (Online y Store)
- Estacionalidad: picos en julio (Fiestas Patrias) y en noviembre-diciembre
- Crecimiento de ~20 % en 2025 respecto a 2024
# scripts/generate_data.py (fragmento) dates = pd.date_range("2024-01-01", "2025-12-31", freq="D") weights = np.ones(len(dates)) weights[dates.month.isin([7])] *= 1.4 # Fiestas Patrias weights[dates.month.isin([11, 12])] *= 1.8 # Campaña navideña weights[dates.year == 2025] *= 1.2 # Crecimiento anual weights /= weights.sum() order_dates = rng.choice(dates, size=N_ORDERS, p=weights) Paso 2: La lógica de negocio (testeable)
En src/data.py están las funciones puras que usa el dashboard:
def filter_sales(df, start, end, regions=None, categories=None, channels=None): mask = (df["order_date"].dt.date >= start) & (df["order_date"].dt.date <= end) if regions: mask &= df["region"].isin(regions) if categories: mask &= df["category"].isin(categories) if channels: mask &= df["channel"].isin(channels) return df.loc[mask] def compute_kpis(df): revenue = float(df["revenue"].sum()) profit = float(df["profit"].sum()) orders = int(df["order_id"].nunique()) return { "revenue": revenue, "profit": profit, "orders": orders, "avg_ticket": revenue / orders if orders else 0.0, "margin": profit / revenue if revenue else 0.0, } Si un filtro está vacío, se toma como "todos". Además, compute_kpis no divide entre cero cuando no hay datos. Ambos casos tienen su test.
Paso 3: El dashboard con Streamlit + Plotly
Carga con caché y filtros en la barra lateral
import plotly.express as px import streamlit as st from src.data import breakdown, compute_kpis, filter_sales, load_sales, monthly_revenue st.set_page_config(page_title="Sales Dashboard", page_icon="📊", layout="wide") @st.cache_data def get_data(): return load_sales() df = get_data() st.sidebar.header("Filters") min_d, max_d = df["order_date"].min().date(), df["order_date"].max().date() date_range = st.sidebar.date_input("Date range", (min_d, max_d), min_value=min_d, max_value=max_d) start, end = date_range if len(date_range) == 2 else (min_d, max_d) regions = st.sidebar.multiselect("Region", sorted(df["region"].unique())) categories = st.sidebar.multiselect("Category", sorted(df["category"].unique())) channels = st.sidebar.multiselect("Channel", sorted(df["channel"].unique())) data = filter_sales(df, start, end, regions, categories, channels) @st.cache_data hace que el CSV se lea una sola vez, aunque el script se vuelva a ejecutar con cada clic.
Tarjetas de KPIs
k = compute_kpis(data) c1, c2, c3, c4, c5 = st.columns(5) c1.metric("Revenue", f"S/ {k['revenue']:,.0f}") c2.metric("Profit", f"S/ {k['profit']:,.0f}") c3.metric("Orders", f"{k['orders']:,}") c4.metric("Avg. ticket", f"S/ {k['avg_ticket']:,.2f}") c5.metric("Margin", f"{k['margin']:.1%}") Gráficos
Usé Plotly Express porque sus gráficos ya son interactivos (zoom, hover, ocultar series):
monthly = monthly_revenue(data) fig_trend = px.line(monthly, x="month", y=["revenue", "profit"], markers=True) fig_trend.update_layout(hovermode="x unified") st.plotly_chart(fig_trend, width="stretch") El dashboard final tiene:
- Tendencia mensual de ingresos y utilidad
- Ingresos por categoría (barras horizontales)
- Ingresos por región (barras)
- Online vs. tienda (dona)
- Mapa de calor categoría × región
- Tabla de pedidos filtrados con botón para descargar CSV Para probarlo en local:
pip install -r requirements-dev.txt streamlit run app.py Paso 4: Tests automáticos
Hay dos tipos de pruebas:
Unitarias, para la lógica de datos:
def test_monthly_revenue_totals_match(df): m = monthly_revenue(df) assert m["revenue"].sum() == pytest.approx(df["revenue"].sum()) assert len(m) == 24 # ene-2024 .. dic-2025 Smoke test de la app completa, con AppTest. Streamlit ejecuta la app sin navegador y podemos interactuar con los widgets:
from streamlit.testing.v1 import AppTest def test_region_filter_updates_kpis(): at = AppTest.from_file(APP, default_timeout=60).run() total_orders = at.metric[2].value at.sidebar.multiselect[0].select("Cusco").run() assert not at.exception assert at.metric[2].value != total_orders Así compruebo que la app no lanza excepciones y que los filtros cambian los KPIs.
Paso 5: Despliegue automatizado con GitHub Actions
Esta es la parte clave. Streamlit Community Cloud redespliega la app cada vez que cambia la rama a la que está conectada. Si la conecto a main, cualquier error llegaría directo a producción.
La solución: conectar Streamlit Cloud a una rama production que solo GitHub Actions actualiza, y solo si los tests pasan.
push a main ──► GitHub Actions: ruff + pytest │ (solo si todo pasa ✅) ▼ promueve el commit a la rama `production` │ ▼ Streamlit Community Cloud detecta el cambio ──► redeploy │ ▼ Actions verifica que la URL pública responda HTTP 200 # .github/workflows/ci-cd.yml name: CI/CD on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: jobs: test: name: Lint & test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip cache-dependency-path: requirements-dev.txt - run: pip install -r requirements-dev.txt - run: ruff check . - run: pytest -v deploy: name: Deploy to Streamlit Community Cloud needs: test if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: contents: write environment: name: production url: ${{ vars.APP_URL }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Promote tested commit to the production branch run: git push origin HEAD:refs/heads/production --force - name: Wait for Streamlit Cloud and check the public URL if: vars.APP_URL != '' run: | sleep 60 for i in 1 2 3 4 5; do code=$(curl -s -o /dev/null -w "%{http_code}" -L "${{ vars.APP_URL }}") echo "Attempt $i -> HTTP $code" [ "$code" = "200" ] && exit 0 sleep 30 done exit 1 Puntos importantes:
-
needs: test→ el job de deploy espera a que pasen los tests. -
if: ... refs/heads/main→ los pull requests se prueban, pero no se despliegan. -
permissions: contents: write→ permite que el workflow haga push a la ramaproduction.
- environment: production → GitHub muestra el historial de despliegues y el link de la app en la página del repo.
Paso 6: Publicar en Streamlit Community Cloud
- Entra a share.streamlit.io e inicia sesión con GitHub.
- Haz clic en Create app → Deploy a public app from GitHub.
- Configura:
- Repository:
IkerASierraR/streamlit-sales-dashboard - Branch:
production - Main file path:
app.py - App URL:
iker-sales-dashboard
- Repository:
- Haz clic en Deploy. Streamlit instala
requirements.txty en uno o dos minutos la app está en línea. - En GitHub, ve a Settings → Secrets and variables → Actions → Variables y crea
APP_URLcon la URL pública. Así se activa la verificación automática. Desde ese momento, el flujo de trabajo es:
git add . git commit -m "feat: add new chart" git push …y en unos minutos el cambio está publicado. Sin tocar ningún servidor. 🚀
Resultado
✅ Dashboard interactivo con 5 KPIs, 5 gráficos, filtros y descarga de CSV
✅ Repositorio público con código, tests y documentación
✅ Pipeline de CI/CD: lint → tests → deploy → verificación
✅ App pública y gratuita en Streamlit Community Cloud
🔗 Pruébalo aquí: https://iker-sales-dashboard.streamlit.app
Conclusiones
- Streamlit permite pasar de un análisis en pandas a una app web en muy pocas líneas.
- Separar la lógica de la interfaz hace que el dashboard sea fácil de probar.
- Una rama
productioncontrolada por CI es una forma simple y gratuita de tener despliegue continuo seguro en Streamlit Community Cloud. - La misma idea sirve para otras herramientas: Dash o Shiny en Render, Hugging Face Spaces, Railway, etc. Si te sirvió, deja un ❤️ o un comentario. ¡Gracias por leer!