lección 2
Día 1 - Explorar eventos y definir "usuario activo"
Abres los JSON de eventos por primera vez, descubres la estructura, negocias la definición de activo con Sandra y Lucia, y diseñas la tabla staging.
⏱ 50 min
### Martes, 8:45 AM - Los eventos que nadie ha tocado
Segundo día. Te sientas con el café (cortado, dos de azúcar -- el de la máquina de TaskFlow es sorprendentemente bueno) y abres la terminal. Ayer Marcos te dio accesos al bucket de S3. Dentro de s3://taskflow-events/raw/ hay 47 carpetas -- una por tipo de evento. Los nombres ya te cuentan una historia: task_created, task_completed, task_assigned, board_viewed, comment_added, workspace_member_invited, subscription_upgraded... Seis meses de comportamiento de 50.000 usuarios, acumulándose en silencio.
1#data-team | Martes 8:50 AM23Tu 8:50 AM4Buenos días! Hoy planeo:5- Explorar los eventos de S3 (estructura, volumenes, anomalias)6- Hablar con Sandra sobre la definición de "usuario activo"7- Empezar el diseño de la tabla staging89Si necesito algo de alguien os aviso ??1011Pablo Herrera 8:52 AM12?? Buen plan. Un consejo: cuando explores los eventos, busca13cosas raras. Timestamps fuera de rango, campos vacios,14duplicados. Es la primera vez que alguien mira esos datos15con ojo crítico. Seguro que hay sorpresas.1617Sandra Vidal 8:54 AM18Cuando llegues a la parte de definición de "activo" buscame ??19Tengo opinion fuerte al respecto y quiero que estemos20alineados ANTES de que construyas la tabla.2122 ?? 2
En FreshMart aprendiste la regla de oro: nunca transformes datos que no has explorado primero. Descargas un sample -- los eventos del día anterior para 3 tipos: task_created, board_viewed y comment_added. Unos 250.000 eventos en un archivo JSON Lines.
1{"event_id": "evt_7f3a2b1c", "event_type": "task_created", "timestamp": "2024-03-14T09:23:41.123Z", "user_id": "usr_4521", "workspace_id": "ws_0892", "properties": {"task_id": "tsk_18923", "board_id": "brd_0445", "has_assignee": true, "has_due_date": false, "priority": "medium"}}2{"event_id": "evt_8d4c3e2f", "event_type": "task_created", "timestamp": "2024-03-14T09:23:41.456Z", "user_id": "usr_4521", "workspace_id": "ws_0892", "properties": {"task_id": "tsk_18924", "board_id": "brd_0445", "has_assignee": true, "has_due_date": true, "priority": "high"}}3{"event_id": "evt_9e5d4f3a", "event_type": "board_viewed", "timestamp": "2024-03-14T09:24:02.789Z", "user_id": "usr_1107", "workspace_id": "ws_0334", "properties": {"board_id": "brd_0201", "view_duration_ms": 34521, "cards_visible": 23}}4{"event_id": "evt_1a2b3c4d", "event_type": "comment_added", "timestamp": "2024-03-14T09:25:11.012Z", "user_id": "usr_2203", "workspace_id": "ws_0892", "properties": {"task_id": "tsk_18900", "comment_length": 142, "has_mention": true}}
Muestra de eventos: JSON Lines, un evento por línea, estructura consistente
Cada línea es un evento independiente. Estructura consistente: event_id único, tipo, timestamp ISO en UTC, user_id del usuario que lo disparó, workspace al que pertenece, y un objeto properties con detalles específicos del tipo de evento. El campo properties varía según el evento: task_created tiene priority y has_due_date; board_viewed tiene view_duration_ms; comment_added tiene comment_length y has_mention.
Esto es diferente a los CSVs de FreshMart donde cada archivo tenía columnas fijas. Aquí el MISMO formato contiene eventos con propiedades DIFERENTES. Es como si el CSV de ventas y el CSV de clientes estuvieran mezclados en un solo archivo, diferenciados solo por un campo "type". Eso tiene implicaciones para el diseño de la tabla: o haces una tabla por tipo de evento (47 tablas!) o haces una tabla unificada con un campo JSON para propiedades variables. Spoiler: vais a ir por la segunda opción.
### 9:00 AM - Exploracion con Pandas
Abres VS Code, creas un notebook de exploración y empiezas a investigar. Tu objetivo: entender volúmenes, tipos de eventos, distribución temporal y detectar anomalías. Es el mismo workflow que usabas con los CSVs de María, pero aquí los datos son JSON y son órdenes de magnitud más grandes.
Te acuerdas de lo que Elena te enseñó en FreshMart: "Antes de escribir una sola línea de transformación, responde estas 5 preguntas sobre los datos: cuántas filas hay, cuántas columnas, qué tipo de dato tiene cada una, cuántos NULLs hay, y hay duplicados?" Aquí aplica igual, solo que en vez de un CSV de María tienes 250K líneas de JSON.
1# explorar_eventos.py - Primera exploracion de eventos TaskFlow2import pandas as pd3import json4from collections import Counter56# Cargar JSON Lines (un JSON por linea)7events = []8with open('datos/eventos_producto.jsonl', 'r') as f:9 for line in f:10 events.append(json.loads(line))1112df = pd.DataFrame(events)1314print(f"Total eventos en muestra (3 días): {len(df):,}")15print(f"Columnas: {list(df.columns)}")16print(f"Tipos de dato:\n{df.dtypes}")17print(f"\nTipos de evento únicos: {df['event_type'].nunique()}")18print(f"\nTop 10 eventos por volumen:")19print(df['event_type'].value_counts().head(10))20print(f"\nUsuarios únicos: {df['user_id'].nunique():,}")21print(f"Workspaces únicos: {df['workspace_id'].nunique():,}")2223# Rango temporal24df['timestamp'] = pd.to_datetime(df['timestamp'])25print(f"\nRango temporal:")26print(f" Min: {df['timestamp'].min()}")27print(f" Max: {df['timestamp'].max()}")28print(f" Dias cubiertos: {(df['timestamp'].max() - df['timestamp'].min()).days}")2930# Buscar anomalias basicas31print(f"\nNulls por columna:")32print(df.isnull().sum())33print(f"\nDuplicados por event_id: {df['event_id'].duplicated().sum():,}")
La primera exploración revela la escala: 750K eventos en 3 días
El resultado revela la escala: en 3 días hay ~750.000 eventos de 47 tipos distintos, generados por ~12.000 usuarios en ~3.400 workspaces. Los más frecuentes son board_viewed (28%), user_login (21%), task_created (15%), task_completed (12%) y comment_added (10%). Y la primera sorpresa: hay 1.847 event_ids duplicados. Los eventos duplicados existen. David te lo va a explicar el miércoles.
### 10:00 AM - La primera sorpresa: duplicados
1#data-eng | Martes 10:05 AM23Tu 10:05 AM4Pregunta para quien sepa: estoy explorando los eventos en S35y veo 1.847 event_ids duplicados en una muestra de 3 días.6Es esperado o es un bug?78Marcos Delgado 10:08 AM9Esperado. Kafka at-least-once delivery. Preguntale a David.1011Sandra Vidal 10:09 AM12Si, me he encontrado con eso antes ?? Cuando hagas el staging13necesitas deduplicar por event_id. Usa ROW_NUMBER() particionado14por event_id ordenado por timestamp, queda con el primero.1516Tu 10:11 AM17Perfecto, gracias a los dos. Lo tendere en cuenta en el diseño18de la tabla.1920 ?? 1
Consejo de senior: en sistemas event-driven, la deduplicación no es un edge case -- es un REQUISITO fundamental. Kafka, SQS, Kinesis... todos tienen escenarios donde un mensaje se entrega más de una vez. Si tu pipeline no deduplica, tus métricas están infladas. El patrón estándar es: INSERT con deduplicación por ID natural del evento + timestamp como desempate.
### 10:30 AM - La reunion con Sandra: que es un "usuario activo"
A las 10:30 Sandra te llama por Meet. Vio tu mensaje sobre los duplicados en #data-eng y quiere aprovechar para alinear algo crítico ANTES de que empieces a construir tablas. Comparte pantalla con su Miro abierto -- un diagrama con flechas y post-its.
1Sandra Vidal (Meet, 10:30 AM):23"Oye, antes de que hagas nada con esos eventos necesitamos4hablar de la definición de usuario activo. Ahora mismo hay5TRES definiciones diferentes circulando por la empresa:671. Lucia dice: usuario activo = alguien que ha hecho login.8 Problema: login no implica uso real. Puedes hacer login,9 mirar el dashboard 2 segundos y irte.10112. El CEO mira Amplitude: cuenta cualquier pageview.12 Problema: un bot o un script de healthcheck cuenta como13 usuario activo.14153. Mi propuesta: activo = alguien que ha hecho una ACCION16 con INTENCION. Crear tarea, completar tarea, comentar,17 asignar, subir archivo. No solo mirar.1819Si no decidimos ahora, vamos a tener 3 números de retención20diferentes y eso es PEOR que no tener ninguno."2122Tu: "Tiene sentido. Y si definimos dos niveles? Core active23para acciones con intencion, y broad active que incluye24también board_viewed y search_performed? Asi reportamos el25core para retención pero tenemos el broad para contexto."2627Sandra: "...me gusta mucho eso. Es lo que en Cabify28llamabamos L1 activity y L2 activity. Vamos con eso.29Haz un listado de que eventos entran en cada categoría30y me lo pasas. Lo documento en el Notion de definiciones31y se lo presentamos a Pablo juntos."3233Tu: "Hecho. Te lo tengo en una hora."3435Sandra: "Perfecto. Y oye... buena propuesta. Se nota que36no es tu primer rodeo." (sonrie)
Ese último comentario de Sandra es importante. Recuerda que empezó con cierta desconfianza -- el trauma del junior anterior que rompio 4 modelos. Que te diga "se nota que no es tu primer rodeo" significa que tu approach de PREGUNTAR PRIMERO está funcionando. En FreshMart habrias ido directo a la query. Aqui preguntas, propones y alineas. Eso es crecer.
### 11:30 AM - Clasificar los 47 tipos de evento
Vuelves a tu exploracion. Necesitas clasificar cada tipo de evento en las categorías que acordaste con Sandra. Iteras sobre los event_types únicos y los agrupas:
1# clasificar_eventos.py - Mapeo de eventos a categorías2# Definicion acordada con Sandra (y que presentaremos a Pablo)34CORE_ACTIVE_EVENTS = [5 'task_created', # Usuario crea una tarea6 'task_completed', # Usuario marca tarea como completada7 'task_assigned', # Usuario asigna tarea a alguien8 'comment_added', # Usuario escribe un comentario9 'file_uploaded', # Usuario sube un archivo10 'board_created', # Usuario crea un tablero nuevo11 'automation_created', # Usuario crea una automatización12]1314BROAD_ACTIVE_EVENTS = CORE_ACTIVE_EVENTS + [15 'board_viewed', # Ve un tablero (intencion ambigua)16 'task_viewed', # Ve el detalle de una tarea17 'search_performed', # Busca algo en el workspace18 'filter_applied', # Aplica un filtro en un board19 'notification_clicked', # Hace click en una notificación20 'dashboard_viewed', # Ve un dashboard de proyecto21 'report_exported', # Exporta un reporte22]2324SYSTEM_EVENTS = [25 'user_login', # Login (no implica uso)26 'session_started', # Sesion técnica27 'session_ended', # Fin de sesion28 'email_opened', # Abrio un email transaccional29 'push_notification_sent', # Se le envio un push30 'password_reset_requested', # Pidio reset de password31 'workspace_member_invited', # Invito a alguien (admin action)32 'subscription_upgraded', # Cambio de plan (billing)33 'subscription_downgraded', # Bajada de plan34 'invoice_generated', # Factura generada35]3637# Verificar cobertura38all_event_types = set(df['event_type'].unique())39classified = set(CORE_ACTIVE_EVENTS + BROAD_ACTIVE_EVENTS + SYSTEM_EVENTS)40unclassified = all_event_types - classified4142print(f"Tipos de evento totales: {len(all_event_types)}")43print(f"Clasificados: {len(classified)}")44print(f"Sin clasificar: {len(unclassified)}")45if unclassified:46 print(f" Pendientes: {sorted(unclassified)}")
Clasificación de eventos - la base de toda métrica de actividad
Al ejecutarlo descubres que hay 8 tipos de evento sin clasificar. Algunos son obvios (task_updated, task_deleted) pero otros son ambiguos (integration_connected, template_applied). Le mandas la lista a Sandra por Slack y ella responde en 10 minutos con la clasificación completa. Trabajar con alguien que CONOCE el dominio acelera todo.
### 14:00 PM - Diseñar la tabla de eventos en Redshift
Después de comer, toca la parte de ingeniería: diseñar la tabla donde van a vivir los eventos en Redshift. Sandra te dice que ella construirá los modelos dbt encima, pero necesita que TU diseñes la tabla staging que alimenta todo. Piensas en lo que necesitas:
- Queries de retención: cuando fue la primera y última actividad de un usuario
- Analisis de features: que eventos hacen los que se quedan vs los que se van
- Volumenes: cuantos eventos por día/hora/tipo para monitoring
- Deduplicacion: event_id como clave natural para evitar duplicados de Kafka
- Flexibilidad: el campo properties varia por tipo de evento
1-- DDL: Tabla staging de eventos de producto en Redshift2-- Convencion de Sandra: stg_product_events3-- El equipo downstream (Sandra con dbt) construira modelos encima45CREATE TABLE staging.stg_product_events (6 -- Identificacion del evento7 event_id VARCHAR(32) NOT NULL, -- PK natural, dedup key8 event_type VARCHAR(64) NOT NULL, -- task_created, board_viewed...9 event_category VARCHAR(16) NOT NULL, -- 'core', 'broad', 'system'1011 -- Temporalidad12 event_timestamp TIMESTAMP NOT NULL, -- UTC siempre13 event_date DATE NOT NULL, -- Particion logica1415 -- Contexto del usuario16 user_id VARCHAR(32) NOT NULL,17 workspace_id VARCHAR(32) NOT NULL,1819 -- Propiedades frecuentes aplanadas (para queries rapidas)20 task_id VARCHAR(32), -- NULL si no aplica21 board_id VARCHAR(32), -- NULL si no aplica2223 -- JSON crudo para propiedades adicionales24 properties_json VARCHAR(4096), -- JSON string original2526 -- Metadatos de ingesta (trazabilidad)27 ingested_at TIMESTAMP DEFAULT GETDATE(),28 source_file VARCHAR(256), -- Ruta en S3 del archivo fuente2930 PRIMARY KEY (event_id)31)32DISTSTYLE KEY33DISTKEY (user_id) -- Queries agrupan por usuario (cohortes)34SORTKEY (event_timestamp) -- Queries filtran por rango de fecha35;
DDL de la tabla staging - decisiones de DISTKEY y SORTKEY explicadas
Consejo de senior: en Redshift, DISTKEY y SORTKEY son las dos decisiones más importantes de rendimiento. DISTKEY(user_id) coloca todos los eventos del mismo usuario en el mismo nodo, acelerando JOINs y GROUP BY por usuario. SORTKEY(event_timestamp) ordena fisicamente por fecha, acelerando filtros WHERE event_timestamp BETWEEN. Para eventos de producto está combinacion es casi siempre correcta.
### 15:30 PM - El imprevisto: Lucía quiere un número ya
Estás terminando de documentar el DDL cuando llega un DM de Lucía. Su estilo inconfundible:
1Lucia Fernandez 15:32 PM2Ey, Pablo me dijo que estas con los eventos ??3Pregunta rápida: puedes decirme cuantos usuarios ACTIVOS4tuvimos la semana pasada? Es para una slide de mañana.5No necesita ser exacto-exacto, un orden de magnitud.67Tu 15:45 PM8Hola Lucia! Estoy en ello. Una pregunta antes de darte9el número: cuando dices "activos", te refieres a acciones10core (crear/completar tareas, comentar) o cualquier uso11incluyendo solo ver boards?1213Lo preguntó porque estamos definiendo la métrica con Sandra14y queremos asegurarnos de que todos usemos el mismo número.1516Lucia Fernandez 15:47 PM17Mmm buena pregunta ??18Para la slide creo que core tiene más sentido. Los que19DE VERDAD usan el producto, no los que solo miran.20Cuanto te lleva?2122Tu 15:49 PM23Dame 30 min, hago un conteo rápido con la muestra.2425Lucia Fernandez 15:49 PM26Eres un crack ??
Fíjate: Lucía pidió "un número" y tú PREGUNTASTE la definición. Eso es exactamente lo que Sandra te dijo esta mañana. En FreshMart habrías dado el primer número que saliera del COUNT. Aquí ya sabes que un número sin definición es una bomba de relojería.
Haces el conteo rápido con Pandas. Resultado: ~8.200 usuarios core activos la semana pasada. Le mandas a Lucía el número con un caveat explícito: "Esto es un estimado basado en una muestra de 3 días extrapolada. El número exacto lo tendrá cuando el pipeline está listo (jueves)." Lucía responde con un pulgar arriba.
Lucía va a usar ese número en una presentación mañana. Si después resulta que era 6.500 en vez de 8.200 porque tu muestra no era representativa, habrás generado un problema de credibilidad. Siempre que des estimados: (1) di que es un estimado, (2) explica con qué datos lo calculaste, (3) da un rango de confianza si puedes ("entre 7K y 9K"). Lucía es razonable y lo entiende. El problema es cuando alguien DOWNSTREAM copia el número sin el caveat.
### 16:30 PM - Presentar la definición a Pablo
1#data-team | Martes 16:35 PM23Tu 16:35 PM4@Pablo @Sandra - Propuesta de definición de "usuario activo":56CORE ACTIVE: usuario que en un período dado ha realizado al7menos 1 accion con intencion:8- task_created, task_completed, task_assigned9- comment_added, file_uploaded10- board_created, automation_created1112BROAD ACTIVE: core + acciones de consumo:13- board_viewed, task_viewed, search_performed14- filter_applied, notification_clicked15- dashboard_viewed, report_exported1617SYSTEM: no cuenta como actividad18- login, session_started/ended, emails, push, billing1920Para retención usamos CORE. Para engagement general, BROAD.21Sandra está de acuerdo. Lucia también (le pregunte).2223Pablo Herrera 16:42 PM24?? Perfecto. Me gusta que sea una definición dual.25Documentalo en el Notion de métricas y a partir de ahora26esa es la fuente de verdad.27Una cosa: anade la fecha de vigencia. "Desde 2024-03-14,28está es la definición oficial." Asi si en el futuro29cambiamos, queda histórico.3031Sandra Vidal 16:44 PM32+1 ?? Ya lo enlazo desde el schema.yml de dbt.33Bien hecho!3435 ? 2 ?? 1
## ejercicios
Explorar eventos JSON con Pandas
Carga el archivo de eventos de muestra (JSON Lines) y calcula: total de eventos, tipos únicos, top 5 por volumen con porcentaje, usuarios únicos, workspaces únicos y rango temporal.
Clasificar eventos por categoría de actividad
Implementa la clasificacion acordada con Sandra: cada evento es "core", "broad" o "system". Calcula usuarios core activos en los últimos 7 días de la muestra.
Diseñar la tabla de staging de eventos en Redshift
Diseña la tabla staging.stg_product_events con event_id como PK para deduplicación, campos aplanados (task_id, board_id), JSON crudo para propiedades variables, y DISTKEY/SORTKEY optimizados para JOINs por usuario y filtros por fecha.
Documentar la definición de usuario activo
Escribe la documentación oficial de la métrica "usuario activo" siguiendo la estructura que Pablo pidió. Incluye: nombre, definición, eventos incluidos, fecha de vigencia, responsable y notas.
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...