DEV Community

Cover image for Building and Deploying an Interactive Sales Dashboard with Streamlit, GitHub Actions and Streamlit Community Cloud
IKER ALBERTO SIERRA RUIZ
IKER ALBERTO SIERRA RUIZ

Posted on

Building and Deploying an Interactive Sales Dashboard with Streamlit, GitHub Actions and Streamlit Community Cloud

¿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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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,
    }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

@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%}")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

El dashboard final tiene:

  1. Tendencia mensual de ingresos y utilidad
  2. Ingresos por categoría (barras horizontales)
  3. Ingresos por región (barras)
  4. Online vs. tienda (dona)
  5. Mapa de calor categoría × región
  6. Tabla de pedidos filtrados con botón para descargar CSV Para probarlo en local:
pip install -r requirements-dev.txt
streamlit run app.py
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# .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
Enter fullscreen mode Exit fullscreen mode

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 rama production.

- 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

  1. Entra a share.streamlit.io e inicia sesión con GitHub.
  2. Haz clic en Create app → Deploy a public app from GitHub.
  3. Configura:
    • Repository: IkerASierraR/streamlit-sales-dashboard
    • Branch: production
    • Main file path: app.py
    • App URL: iker-sales-dashboard
  4. Haz clic en Deploy. Streamlit instala requirements.txt y en uno o dos minutos la app está en línea.
  5. En GitHub, ve a Settings → Secrets and variables → Actions → Variables y crea APP_URL con 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
Enter fullscreen mode Exit fullscreen mode

…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 production controlada 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!

Top comments (0)