¿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!
Top comments (0)