Analisis de datos reactivo con Marimo y DataSpoc Lens
Los notebooks de Jupyter tienen un secreto oscuro: las celdas pueden ejecutarse en cualquier orden. Puedes tener df definido en la celda 10, usado en la celda 3 y sobreescrito en la celda 7. El estado es invisible. Los bugs estan garantizados.
Marimo solucióna esto. Es un notebook reactivo donde las celdas se re-ejecutan automáticamente cuando sus dependencias cambian. Cambias un filtro en la celda 2 y cada celda que depende de el se actualiza al instante. Combinalo con DataSpoc Lens y obtienes una interfaz reactiva y en vivo para tu data lake.
Que es Marimo?
Marimo es un notebook de Python donde:
- Las celdas se auto-ejecutan cuando cambias una variable ascendente
- Sin estado oculto — el orden de ejecucion lo determina el flujo de datos, no la posicion de las celdas
- Python puro — los notebooks son archivos
.py, no blobs JSON - Elementos de UI integrados — sliders, dropdowns, tablas que son reactivos por defecto
Piensa en el cómo una hoja de calculo para Python: cambias una celda, todo lo que depende de ella se actualiza.
Inicio rápido
Lanza Marimo con DataSpoc Lens:
dataspoc-lens notebook --marimoEsto inicia un servidor Marimo preconfigurado con una conexión DuckDB a tu data lake. Todos tus buckets y tablas estan disponibles inmediatamente.
Alternativamente, instala y lanza manualmente:
pip install marimo dataspoc-lensmarimo editCelda 1: Conectar a tu data lake
import marimo as mofrom dataspoc_lens import LensClient
lens = LensClient()tables = lens.tables()mo.md(f"**Connected.** {len(tables)} tables available.")Salida: Connected. 12 tables available.
Celda 2: Selector interactivo de tablas
table_dropdown = mo.ui.dropdown( options=tables, label="Select a table",)table_dropdownEsto renderiza un dropdown en el notebook. Cuando seleccionas una tabla diferente, cada celda que hace referencia a table_dropdown se re-ejecuta automáticamente.
Celda 3: Mostrar esquema (reactivo)
# This cell re-runs whenever table_dropdown.value changesselected = table_dropdown.valueif selected: schema = lens.schema(selected) schema_table = mo.ui.table( [{"Column": c["name"], "Type": c["type"]} for c in schema], label=f"Schema: {selected}", ) schema_tableSelecciona “raw.orders” del dropdown — esta celda muestra las columnas al instante. Selecciona “raw.customers” — se actualiza. No se necesita boton de re-ejecucion.
Celda 4: Consulta dinamica con filtros
# Build a query based on the selected tableif selected: limit_slider = mo.ui.slider( start=10, stop=1000, step=10, value=100, label="Row limit", ) mo.hstack([limit_slider])# This cell depends on both selected and limit_sliderif selected: sql = f"SELECT * FROM {selected} LIMIT {limit_slider.value}" result = lens.query(sql) mo.ui.table(result.to_pandas())Mueve el slider de 100 a 500 — la consulta se re-ejecuta y la tabla se actualiza. El grafo reactivo se ve asi:
table_dropdown → selected → sql → result → table display ↑ limit_slider ──┘Celda 5: Agregacion con rango de fechas
if selected == "raw.orders": date_range = mo.ui.date_range( start="2026-01-01", stop="2026-04-26", label="Date range", ) date_rangeif selected == "raw.orders" and date_range.value: start, end = date_range.value agg_sql = f""" SELECT DATE_TRUNC('week', order_date) as week, COUNT(*) as orders, SUM(amount) as revenue, ROUND(AVG(amount), 2) as avg_order FROM raw.orders WHERE order_date BETWEEN DATE '{start}' AND DATE '{end}' GROUP BY 1 ORDER BY 1 """ agg_result = lens.query(agg_sql) df = agg_result.to_pandas()
# Reactive chart import altair as alt chart = alt.Chart(df).mark_bar().encode( x="week:T", y="revenue:Q", tooltip=["week", "orders", "revenue", "avg_order"], ).properties(width=600, height=300)
mo.vstack([ mo.ui.table(df), chart, ])Cambia el selector de rango de fechas — la consulta de agregacion se re-ejecuta, la tabla se actualiza, el grafico se redibuja. Todo automático.
La API programatica connect()
Para scripts y automatización, usa connect() para obtener una conexión DuckDB directamente:
import duckdbfrom dataspoc_lens import LensClient
lens = LensClient()conn = lens.connect() # Returns a duckdb.DuckDBPyConnection
# Use standard DuckDB APIresult = conn.execute(""" SELECT plan, COUNT(*) as users FROM raw.customers GROUP BY plan ORDER BY users DESC""").fetchdf()
print(result)Esto es util en celdas de Marimo cuando quieres acceso directo a DuckDB:
# Marimo cell using connect()conn = lens.connect()
# Complex analytical queryfunnel = conn.execute(""" WITH signups AS ( SELECT user_id, MIN(created_at) as signup_date FROM raw.events WHERE event_type = 'signup' GROUP BY user_id ), activations AS ( SELECT user_id, MIN(timestamp) as activation_date FROM raw.events WHERE event_type = 'first_purchase' GROUP BY user_id ) SELECT DATE_TRUNC('week', s.signup_date) as cohort_week, COUNT(DISTINCT s.user_id) as signups, COUNT(DISTINCT a.user_id) as activated, ROUND(100.0 * COUNT(DISTINCT a.user_id) / COUNT(DISTINCT s.user_id), 1) as activation_rate FROM signups s LEFT JOIN activations a ON s.user_id = a.user_id GROUP BY 1 ORDER BY 1""").fetchdf()
mo.ui.table(funnel)Marimo vs. Jupyter: Una comparación real
Considera este flujo de trabajo: explorar ingresos por region, luego profundizar en la region principal.
En Jupyter:
- Celda 1: Cargar datos — Ejecutar
- Celda 2: Agrupar por region — Ejecutar
- Celda 3: Graficar resultados — Ejecutar
- Celda 4: Filtrar por region principal — Ejecutar
- Oh espera, quieres cambiar el rango de fechas en la Celda 1
- Re-ejecutar Celda 1 — manualmente
- Re-ejecutar Celda 2 — manualmente
- Re-ejecutar Celda 3 — manualmente
- Re-ejecutar Celda 4 — manualmente
- Olvidaste alguna celda? El estado esta desactualizado? Quien sabe.
En Marimo:
- Celda 1: Cargar datos con selector de fecha — se ejecuta
- Celda 2: Agrupar por region — se auto-ejecuta
- Celda 3: Graficar resultados — se auto-ejecuta
- Celda 4: Filtrar por region principal — se auto-ejecuta
- Cambiar el selector de fecha en la Celda 1
- Las Celdas 2, 3, 4 se actualizan automáticamente
- El estado siempre es consistente
| Caracteristica | Jupyter | Marimo |
|---|---|---|
| Orden de ejecucion | Manual (cualquier orden) | Automatico (flujo de datos) |
| Estado oculto | Si (problema constante) | No (imposible por diseno) |
| Widgets de UI | ipywidgets (cableado manual) | Integrados, reactivos |
| Formato de archivo | JSON (malos diffs) | Python puro (diffs limpios) |
| Reproducibilidad | Run All y esperar | Garantizada por diseno |
| Integracion DataSpoc | dataspoc-lens notebook | dataspoc-lens notebook --marimo |
Consejos para notebooks reactivos efectivos
- Una salida por celda — las celdas de Marimo devuelven su ultima expresion. Manten las celdas enfocadas.
- Usa elementos de UI para parametros —
mo.ui.slider,mo.ui.dropdown,mo.ui.textson todos reactivos. Usalos en lugar de valores hardcodeados. - Cachea consultas pesadas — usa
lens.cache_refresh(table)antes de sesiones de exploracion para evitar lecturas repetidas a la nube. - Nombra las variables claramente — dado que Marimo rastrea dependencias por nombre de variable, nombres claros hacen el grafo reactivo mas fácil de seguir.
- Usa
mo.stop()— para evitar que una celda se ejecute hasta que se cumpla una condicion:
mo.stop(not table_dropdown.value, "Select a table above to continue.")# Code below only runs when a table is selectedMarimo hace que la exploracion de datos se sienta cómo usar un dashboard, pero con todo el poder de Python y SQL. Combinado con DataSpoc Lens, obtienes una interfaz reactiva a terabytes de datos Parquet en la nube — sin infraestructura, sin estado desactualizado, sin re-ejecuciones manuales.