Streamlit — Módulo 3

Cache · Estado · Layout · Componentes

@josephinoo

2026-04-15

🧠

Módulo 3

Cache · Estado · Layout · Componentes

¿Qué construimos hoy?

Bloque 1 — Cache + Estado
@st.cache_data · session_state · st.spinner · st.progress
🧱
Bloque 2 — Layout
st.columns · st.container · st.tabs · st.expander · st.empty
🎛️
Bloque 3 — Componentes
st.logo · st.image · st.popover · st.dialog
📊
Bloque 4 — Datos avanzados
st.metric · column_config · px.choropleth
🎬
Bloque 5 — Showcase
Netflix Dashboard — todo junto

Bloque 1 — Cache + Estado ⚡

El problema sin caché

# Sin @st.cache_data
def cargar_datos(ruta):
    return pd.read_csv(ruta)

df = cargar_datos("ventas.csv")
1
Usuario abre la app → lee el CSV ⏱️
↓ mueve el slider
2
Rerun → lee el CSV OTRA VEZ ⏱️
↓ cambia un filtro
3
Rerun → lee el CSV OTRA VEZ ⏱️
Con un CSV de 50 MB y un usuario activo → el disco se lee decenas de veces por minuto innecesariamente.
💡 El problema se multiplica cuando hay muchos usuarios conectados al mismo tiempo.

@st.cache_data — la solución

# Con @st.cache_data ✅
@st.cache_data
def cargar_datos(ruta):
    return pd.read_csv(ruta)

df = cargar_datos("ventas.csv")
1
Primera llamada → lee el CSV · guarda en caché
↓ mueve el slider
2
Rerun → devuelve caché ⚡ instantáneo
↓ cambia un filtro
3
Rerun → devuelve caché ⚡ instantáneo
cargar_datos("ventas.csv")
¿está en caché?
✅ Sí → devolver
·
❌ No → ejecutar · guardar
La clave del caché son los argumentos. Si llamas cargar_datos("otro.csv"), calcula de nuevo.

@st.cache_data — parámetros clave

@st.cache_data(
    ttl=3600,        # expira en 1 hora
    max_entries=5,   # máx 5 resultados guardados
    show_spinner="Cargando...",
)
def obtener_datos(año: int):
    # simulación de query lenta
    time.sleep(2)
    return df[df["año"] == año]
# Borrar caché manualmente
if st.button("Actualizar datos"):
    obtener_datos.clear()  # solo esta función
    st.cache_data.clear()  # toda la caché
Parámetro Para qué sirve
ttl Tiempo de vida en segundos
max_entries Límite de entradas
show_spinner Texto de carga
vs @st.cache_resource
Usa cache_resource para objetos que no se deben copiar: conexiones a BD, modelos de ML, clientes de API.

st.session_state — el problema

# Sin session_state ❌
if st.button("Incrementar"):
    contador = contador + 1
    # ↑ NameError: contador no existe
    # o si lo defines arriba:

contador = 0  # se reinicia en cada rerun
if st.button("Incrementar"):
    contador += 1
st.write(contador)  # siempre muestra 0
sin session_state
0

st.session_state — la solución

# Con session_state ✅
if "contador" not in st.session_state:
    st.session_state.contador = 0

if st.button("Incrementar"):
    st.session_state.contador += 1

st.write(st.session_state.contador)
# ↑ persiste entre reruns ✅
st.session_state es un diccionario que sobrevive a los reruns. Cada usuario tiene el suyo — no se comparte.
con session_state ✅
0

st.session_state — patrones comunes

Leer y escribir
# Dos sintaxis equivalentes
st.session_state["clave"]
st.session_state.clave

# Inicializar antes de leer
if "filtro" not in st.session_state:
    st.session_state.filtro = "Todos"

# Widget sincronizado automáticamente
st.selectbox("País", paises, key="pais_sel")
# → st.session_state.pais_sel tiene el valor
Caso típico — estado de navegación
def init_state():
    defaults = {
        "pagina":   "Datos",
        "filtro":   "Todos",
        "busqueda": "",
    }
    for k, v in defaults.items():
        if k not in st.session_state:
            st.session_state[k] = v

init_state()  # llamar al inicio
Centralizar en init_state() evita errores de clave no encontrada.

st.spinner y st.progress

# Spinner — bloque con indicador giratorio
with st.spinner("Procesando datos..."):
    df = pd.read_csv("datos_grandes.csv")
    df = limpiar(df)
    # todo lo del bloque muestra el spinner

# Progress bar — para operaciones paso a paso
barra = st.progress(0, text="Iniciando...")
pasos = ["Leer CSV", "Limpiar", "Calcular"]
for i, paso in enumerate(pasos):
    barra.progress((i+1)/len(pasos), text=paso)
    time.sleep(0.5)
barra.empty()  # quitar al terminar

# st.status — spinner expandible
with st.status("Procesando...", expanded=True) as s:
    st.write("Paso 1: leyendo...")
    time.sleep(1)
    s.update(label="Listo ✅", state="complete")
Listo para iniciar
💡 Usa st.spinner cuando no sabes cuánto va a tardar. Usa st.progress cuando tienes pasos contables.

Bloque 2 — Layout 🧱

st.columns — dividir en columnas

Haz clic para ver distintos layouts:
col 1
col 2
col 3
col 4
# Métricas en 4 columnas — patrón más común del curso
col1, col2, col3, col4 = st.columns(4)
col1.metric("Ventas",    "$45,200", "+12%")
col2.metric("Clientes",  "1,234",   "+8%")
col3.metric("Productos", "89",      "-2")
col4.metric("Países",    "23")

# Proporciones personalizadas [3, 2] = 60% / 40%
col_main, col_side = st.columns([3, 2])
with col_main:
    st.plotly_chart(fig)
with col_side:
    st.dataframe(resumen)

st.container — agrupar y ordenar

# Insertar contenido fuera de orden
header = st.container()       # reservar espacio ①
st.write("Este texto va segundo")

# Llenar el container después ②
header.metric("Total", len(df))
header.write("Este aparece arriba ✅")
# Container con borde visual
with st.container(border=True):
    st.subheader("Panel de control")
    st.slider("Año", 2018, 2024)
    st.selectbox("País", paises)
sin container
texto primero
métrica (definida después)
con container
métrica (aparece aquí ✅)
texto segundo
💡 Útil para KPIs que dependen de datos cargados más abajo en el script.

st.tabs — pestañas de contenido

tab1, tab2, tab3 = st.tabs([
    "Tabla",
    "Gráfico",
    "Resumen"
])

with tab1:
    st.dataframe(df)

with tab2:
    st.plotly_chart(fig)

with tab3:
    st.metric("Total", len(df))
    st.write(df.describe())
💡 Úsalas cuando tienes vistas distintas de los mismos datos — evita el scroll infinito.
Tabla
Gráfico
Resumen
producto · ventas · región
Laptop · $1200 · Norte
Mouse · $25 · Sur
Monitor· $450 · Centro

st.expander y st.empty

Ver datos crudos
fecha,producto,ventas
2024-01,Laptop,45200
2024-01,Mouse,1200
with st.expander("Ver datos crudos"):
    st.dataframe(df)

with st.expander("Filtros avanzados"):
    st.multiselect("Columnas:", df.columns)
    st.slider("Rango de fechas", ...)
st.empty — reemplazar contenido
[ placeholder vacío ]
ph = st.empty()          # reservar espacio

with ph.container():     # llenar
    st.info("Cargando...")
    barra = st.progress(0)

ph.empty()               # limpiar todo

Bloque 3 — Componentes 🎛️

st.logo y st.image

# Logo en la parte superior del sidebar
st.logo(
    "logo.png",
    size="large",
    link="https://miempresa.com"
)

# Imagen estándar
st.image(
    "grafico_exportado.png",
    caption="Gráfico exportado",
    use_container_width=True
)

# Desde URL directa
st.image(
    "https://ejemplo.com/imagen.jpg",
    width=300
)

# Múltiples imágenes en columnas
col1, col2 = st.columns(2)
col1.image("antes.png", caption="Antes")
col2.image("despues.png", caption="Después")
N
Mi Empresa
[ imagen del dashboard ]
Reporte Q1 2024
💡 st.logo es diferente a st.image — aparece fijo en la parte superior del sidebar, no en el flujo principal de la página.
Formatos soportados: PNG, JPG, GIF, SVG, WebP.

st.popover — panel flotante

# Botón que abre un panel flotante
with st.popover("⚙️ Configurar"):
    st.checkbox("Mostrar valores", True)
    st.slider("Opacidad", 0, 100, 80)
    st.color_picker("Color", "#FF4B4B")

# En la misma línea con columnas
col1, col2, _ = st.columns([1, 1, 4])
with col1.popover("Filtros"):
    año = st.selectbox("Año:", [2022,2023,2024])
with col2.popover("Columnas"):
    cols = st.multiselect("Ver:", df.columns)
Ideal para filtros o configuraciones que no necesitan espacio permanente. El usuario los abre solo cuando los necesita.
⚙️ Configurar ▾
Los cambios se aplican en tiempo real
💡 Los widgets dentro del popover funcionan exactamente igual que fuera — cada cambio dispara un rerun.

st.dialog — ventana modal

# Decorar una función para convertirla en modal
@st.dialog("Confirmar acción", width="small")
def confirmar_borrado(nombre: str):
    st.warning(f"¿Borrar '{nombre}'?")
    col1, col2 = st.columns(2)
    if col1.button("Sí, borrar", type="primary"):
        borrar(nombre)
        st.rerun()
    if col2.button("Cancelar"):
        st.rerun()  # cierra el modal

# Invocar desde un botón
fila_sel = st.selectbox("Registro:", registros)
if st.button("Borrar registro"):
    confirmar_borrado(fila_sel)  # abre el modal
El modal corre de forma independiente del resto de la app — solo se rerenderiza lo que está dentro.
Confirmar acción
⚠️ ¿Estás seguro de que quieres borrar el registro "Laptop Pro"?

Esta acción no se puede deshacer.
💡 Úsalo para confirmaciones de borrado, formularios de edición, detalle de un registro.

Bloque 4 — Datos Avanzados 📊

st.metric — KPIs con delta

$45,200
Ventas del mes
▲ +12% vs mes anterior
1,234
Clientes activos
▲ +8%
89
Productos
▼ -2 esta semana
23
Países
sin cambios
col1, col2, col3, col4 = st.columns(4)

col1.metric("Ventas del mes",    "$45,200",  "+12%")
col2.metric("Clientes activos",  "1,234",    "+8%")
col3.metric("Productos",         "89",       "-2",
            delta_color="inverse")  # rojo si positivo
col4.metric("Países",            "23")
💡 delta_color="inverse" invierte la lógica de color — útil para métricas donde subir es malo (costos, errores, devoluciones).

column_config — tablas con superpoderes

Tipo Cuándo usarlo Ejemplo
TextColumn Texto con ancho personalizado TextColumn("Nombre", width="large")
NumberColumn Números con formato moneda/unidad NumberColumn("Precio", format="$%.2f")
DateColumn Fechas con formato legible DateColumn("Fecha", format="DD/MM/YYYY")
ProgressColumn Valores relativos con barra visual ProgressColumn("Ventas", max_value=1000)
LinkColumn URLs clicables LinkColumn("Fuente", display_text="Ver")
ImageColumn Miniaturas de imágenes ImageColumn("Foto", width="small")
st.dataframe(df, column_config={
    "producto":  st.column_config.TextColumn("Producto", width="large"),
    "precio":    st.column_config.NumberColumn("Precio ($)", format="$%.2f"),
    "fecha":     st.column_config.DateColumn("Fecha", format="DD/MM/YYYY"),
    "ventas":    st.column_config.ProgressColumn("Ventas",
                     min_value=0, max_value=int(df.ventas.max())),
})

px.choropleth — mapa mundial

import plotly.express as px

# Necesitas códigos ISO alpha-3
df_paises = pd.DataFrame({
    "pais": ["USA", "CHN", "DEU", "BRA"],
    "valor": [100, 85, 62, 45]
})

fig = px.choropleth(
    df_paises,
    locations="pais",   # columna con código ISO
    color="valor",      # columna a visualizar
    hover_name="pais",
    color_continuous_scale="Reds",
    title="Distribución mundial"
)

fig.update_layout(
    geo=dict(showframe=False,
             bgcolor="rgba(0,0,0,0)"),
    paper_bgcolor="rgba(0,0,0,0)",
    font_color="#e8e8e8"
)

st.plotly_chart(fig, use_container_width=True)
Conversión de nombres a ISO alpha-3
Plotly necesita códigos como USA, CHN, DEU — no texto libre como "United States".
import pycountry

def a_iso3(nombre):
    try:
        return pycountry.countries\
            .search_fuzzy(nombre)[0].alpha_3
    except:
        return None

df["iso"] = df["pais"].apply(a_iso3)
pip install pycountry

Bloque 5 — Showcase 🎬

El dashboard que pusimos juntos

Overview
Películas
Series
Directores
Países
8,803
Títulos
6,131
Películas
2,676
Series
748
Países
[ Timeline — títulos por año · barras apiladas Movie + TV Show ]

Dataset: netflix_titles.csv de Kaggle · kaggle.com/datasets/shivamb/netflix-shows · 8803 filas · gratuito

El código — estructura del archivo

# netflix_dashboard.py
import streamlit as st
import pandas as pd
import plotly.express as px

st.set_page_config(page_title="Netflix",
    page_icon="🎬", layout="wide")

# ① Cache — carga una sola vez
@st.cache_data
def cargar(ruta):
    df = pd.read_csv(ruta)
    df["date_added"] = pd.to_datetime(
        df["date_added"].str.strip(), errors="coerce")
    df["año_agregado"] = df["date_added"].dt.year
    df["pais"] = df["country"].str.split(",") \
                              .str[0].str.strip()
    return df

# ② Dialog — detalle de un título
@st.dialog("Detalle", width="large")
def ver_titulo(titulo, df):
    fila = df[df["title"] == titulo].iloc[0]
    st.subheader(fila["title"])
    st.caption(fila["type"]+" · "+str(fila["release_year"]))
    st.write(fila["description"])
# ③ Session State
if "tipo" not in st.session_state:
    st.session_state.tipo = "Todos"

# ④ Sidebar con filtros
with st.sidebar:
    st.markdown("# 🎬 Netflix")
    tipo = st.radio("Tipo:", ["Todos","Movie","TV Show"])
    with st.popover("Más filtros"):
        año = st.slider("Año:", 1925, 2021, (2000,2021))

# ⑤ Spinner + carga
with st.spinner("Cargando..."):
    df = cargar("netflix_titles.csv")

# ⑥ Filtros aplicados
if tipo != "Todos": df = df[df["type"]==tipo]

# ⑦ KPIs
c1,c2,c3,c4 = st.columns(4)
c1.metric("Títulos",   len(df))
c2.metric("Películas", (df.type=="Movie").sum())
c3.metric("Series",    (df.type=="TV Show").sum())
c4.metric("Países",    df["pais"].nunique())

# ⑧ Tabs con todo el contenido
t1,t2,t3 = st.tabs(["Overview","Películas","Países"])
with t1: ...
with t2: ...
with t3: ...

Para ejecutarlo

1. Descargar el dataset
🔗 kaggle.com/datasets/shivamb/netflix-shows
Archivo: netflix_titles.csv
2. Estructura del proyecto
mi-proyecto/
  ├── netflix_dashboard.py
  └── netflix_titles.csv
3. Instalar dependencias
pip install streamlit plotly pandas pycountry
4. Ejecutar
$ streamlit run netflix_dashboard.py
✅ El dashboard se abre automáticamente en el navegador en localhost:8501
💡 Si no tienes cuenta en Kaggle, créala gratis — solo necesitas email. El dataset es público y gratuito.

Resumen del Módulo 3 🏁

Todo lo que aprendiste hoy

01
@st.cache_data
Ejecuta la función una vez. Devuelve caché en los reruns siguientes.
🧠
02
session_state
Memoria entre reruns. Cada usuario tiene la suya. Inicializar antes de leer.
🔄
03
spinner · progress
with st.spinner para duración desconocida. st.progress para pasos contables.
📐
04
columns · container
Dividir pantalla en columnas. Container para agrupar y controlar el orden.
📑
05
tabs · expander
Tabs para múltiples vistas del mismo dato. Expander para ocultar contenido secundario.
📌
06
st.empty
Reservar espacio. Reemplazar con contenido real. Limpiar con .empty().
🖼️
07
logo · image
st.logo fijo en sidebar. st.image en el flujo normal de la página.
💬
08
popover · dialog
Popover para filtros flotantes. Dialog modal para confirmaciones y formularios.
📊
09
metric · column_config
KPIs con delta. Tablas enriquecidas con Progress, Date, Number, Link columns.

Tu producto parcial ✔️

✅ App con Cache, Estado y Layout avanzado
@st.cache_data en todas las funciones de carga de datos
st.session_state para filtros y estado global
st.spinner o st.progress al cargar datos
st.tabs para organizar las vistas del dashboard
st.columns(4) con st.metric para KPIs superiores
Al menos un st.expander o st.popover en la UI
column_config con al menos un tipo especial en la tabla
$ streamlit run app.py
✅ Módulo 3 Completo

¡A construir! 🧠

Módulo 3 — Completado

Preguntas antes de cerrar 🙋