Saltar al contenido

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 Lucía, y diseñas la tabla staging.

⏱ 55 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 28 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... Siete meses de comportamiento de 8.200 usuarios, acumulándose en silencio.

1#data-team | Martes 8:50
2
3Tú 8:50
4Buenos días! Hoy planeo:
5- Explorar los eventos de S3 (estructura, volúmenes, anomalías)
6- Hablar con Sandra sobre la definición de "usuario activo"
7- Empezar el diseño de la tabla staging
8
9Si necesito algo de alguien os aviso 👍
10
11Pablo Herrera 8:52
12📋 Buen plan. Un consejo: cuando explores los eventos, busca
13cosas raras. Timestamps fuera de rango, campos vacíos,
14duplicados. Es la primera vez que alguien mira esos datos
15con ojo crítico. Seguro que hay sorpresas.
16
17Sandra Vidal 8:54
18Cuando llegues a la parte de definición de "activo" búscame 📊
19Tengo opinión fuerte al respecto y quiero que estemos
20alineados ANTES de que construyas la tabla.
21
22 📊 2

En FreshMart aprendiste la regla de oro: nunca transformes datos que no has explorado primero. Te descargas el extracto de eventos que el equipo publica: siete meses de histórico, todos los tipos, sin filtrar nada. Son 130.740 eventos en un archivo JSON Lines.

1{"event_id": "evt_7f3a2b1c", "event_type": "task_created", "timestamp": "2024-10-04T09: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-10-04T09: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-10-04T09: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-10-04T09: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 (28 tablas!) o haces una tabla unificada con un campo JSON para propiedades variables. Spoiler: vais a ir por la segunda opción.

## 9:00 - Exploración 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 130.740 líneas de JSON.

1# explorar_eventos.py - Primera exploracion de eventos TaskFlow
2import pandas as pd
3import json
4from collections import Counter
5
6# 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))
11
12df = pd.DataFrame(events)
13
14print(f"Total eventos en la muestra: {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():,}")
22
23# Rango temporal
24df['timestamp'] = pd.to_datetime(df['timestamp'])
25print(f"\nRango temporal:")
26print(f" Min: {df['timestamp'].min()}")
27print(f" Max: {df['timestamp'].max()}")
28print(f" Días cubiertos: {df['timestamp'].dt.normalize().nunique()}")
29
30# Buscar anomalias basicas
31print(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: 130.740 eventos en siete meses

El extracto trae 130.740 eventos de 28 tipos distintos, generados por 8.500 usuarios en 1.944 workspaces, entre marzo y octubre. Los más frecuentes son board_viewed (23,5%), user_login (12,5%), task_created (11,7%), task_completed (11,0%) y task_viewed (9,4%). Y la primera sorpresa: hay 3.998 event_ids duplicados. Los eventos duplicados existen. David te lo va a explicar el miércoles.

## 10:00 - La primera sorpresa: duplicados

1#data-eng | Martes 10:05
2
3Tú 10:05
4Pregunta para quien sepa: estoy explorando los eventos en S3
5y veo 3.998 event_ids duplicados en el extracto de siete meses.
6¿Es esperado o es un bug?
7
8Marcos Delgado 10:08
9Esperado. Kafka at-least-once delivery. Pregúntale a David.
10
11Sandra Vidal 10:09
12Sí, me he encontrado con eso antes 📊 Cuando hagas el staging
13necesitas deduplicar por event_id. Usa ROW_NUMBER() particionado
14por event_id ordenado por timestamp, queda con el primero.
15
16Tú 10:11
17Perfecto, gracias a los dos. Lo tendré en cuenta en el diseño
18de la tabla.
19
20 📊 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 - La reunión con Sandra: qué 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):
2
3"Oye, antes de que hagas nada con esos eventos necesitamos
4hablar de la definición de usuario activo. Ahora mismo hay
5TRES definiciones diferentes circulando por la empresa:
6
71. Lucía 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.
10
112. El CEO mira Amplitude: cuenta cualquier pageview.
12 Problema: un bot o un script de healthcheck cuenta como
13 usuario activo.
14
153. Mi propuesta: activo = alguien que ha hecho una ACCIÓN
16 con INTENCIÓN. Crear tarea, completar tarea, comentar,
17 asignar, subir archivo. No solo mirar.
18
19Si no decidimos ahora, vamos a tener 3 números de retención
20diferentes y eso es PEOR que no tener ninguno."
21
22Tú: "Tiene sentido. ¿Y si definimos dos niveles? Core active
23para acciones con intención, y broad active que incluye
24también board_viewed y search_performed? Así reportamos el
25core para retención pero tenemos el broad para contexto."
26
27Sandra: "...me gusta mucho eso. Es lo que en Cabify
28llamábamos L1 activity y L2 activity. Vamos con eso.
29Haz un listado de qué eventos entran en cada categoría
30y me lo pasas. Lo documento en el Notion de definiciones
31y se lo presentamos a Pablo juntos."
32
33Tú: "Hecho. Te lo tengo en una hora."
34
35Sandra: "Perfecto. Y oye... buena propuesta. Se nota que
36no es tu primer rodeo." (sonríe)

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 - Clasificar los 28 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ías
2# Definicion acordada con Sandra (y que presentaremos a Pablo)
3
4CORE_ACTIVE_EVENTS = [
5 'task_created', # Usuario crea una tarea
6 'task_completed', # Usuario marca tarea como completada
7 'task_assigned', # Usuario asigna tarea a alguien
8 'comment_added', # Usuario escribe un comentario
9 'file_uploaded', # Usuario sube un archivo
10 'board_created', # Usuario crea un tablero nuevo
11 'automation_created', # Usuario crea una automatización
12]
13
14BROAD_ACTIVE_EVENTS = CORE_ACTIVE_EVENTS + [
15 'board_viewed', # Ve un tablero (intencion ambigua)
16 'task_viewed', # Ve el detalle de una tarea
17 'search_performed', # Busca algo en el workspace
18 'filter_applied', # Aplica un filtro en un board
19 'notification_clicked', # Hace click en una notificación
20 'dashboard_viewed', # Ve un dashboard de proyecto
21 'report_exported', # Exporta un reporte
22]
23
24SYSTEM_EVENTS = [
25 'user_login', # Login (no implica uso)
26 'session_started', # Sesion técnica
27 'session_ended', # Fin de sesion
28 'email_opened', # Abrio un email transaccional
29 'push_notification_sent', # Se le envio un push
30 'password_reset_requested', # Pidio reset de password
31 'workspace_member_invited', # Invito a alguien (admin action)
32 'subscription_upgraded', # Cambio de plan (billing)
33 'subscription_downgraded', # Bajada de plan
34 'invoice_generated', # Factura generada
35]
36
37# Verificar cobertura
38all_event_types = set(df['event_type'].unique())
39classified = set(CORE_ACTIVE_EVENTS + BROAD_ACTIVE_EVENTS + SYSTEM_EVENTS)
40unclassified = all_event_types - classified
41
42print(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 quedan 6 tipos de evento sin clasificar. Algunos son obvios (task_updated, task_deleted) y otros son ambiguos (integration_connected, template_applied, workspace_member_added). Y uno te deja pensando: task_updatedXX, con dos letras pegadas al final — alguien se equivocó al escribir el nombre del evento en el tracking, y ahora hay dos tipos donde debería haber uno. Le mandas la lista a Sandra por Slack y ella responde en diez minutos con la clasificación completa. Trabajar con alguien que CONOCE el dominio acelera todo.

## 14:00 - 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 Redshift
2-- Convencion de Sandra: stg_product_events
3-- El equipo downstream (Sandra con dbt) construira modelos encima
4
5CREATE TABLE staging.stg_product_events (
6 -- Identificacion del evento
7 event_id VARCHAR(32) NOT NULL, -- PK natural, dedup key
8 event_type VARCHAR(64) NOT NULL, -- task_created, board_viewed...
9 event_category VARCHAR(16) NOT NULL, -- 'core', 'broad', 'system'
10
11 -- Temporalidad
12 event_timestamp TIMESTAMP NOT NULL, -- UTC siempre
13 event_date DATE NOT NULL, -- Particion logica
14
15 -- Contexto del usuario
16 user_id VARCHAR(32) NOT NULL,
17 workspace_id VARCHAR(32) NOT NULL,
18
19 -- Propiedades frecuentes aplanadas (para queries rapidas)
20 task_id VARCHAR(32), -- NULL si no aplica
21 board_id VARCHAR(32), -- NULL si no aplica
22
23 -- JSON crudo para propiedades adicionales
24 properties_json VARCHAR(4096), -- JSON string original
25
26 -- Metadatos de ingesta (trazabilidad)
27 ingested_at TIMESTAMP DEFAULT GETDATE(),
28 source_file VARCHAR(256), -- Ruta en S3 del archivo fuente
29
30 PRIMARY KEY (event_id)
31)
32DISTSTYLE KEY
33DISTKEY (user_id) -- Queries agrupan por usuario (cohortes)
34SORTKEY (event_timestamp) -- Queries filtran por rango de fecha
35;

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.

Una advertencia sobre la PRIMARY KEY del DDL: Redshift la acepta pero no la comprueba. A diferencia de Postgres o MySQL, las restricciones de clave primaria en Redshift son informativas —le sirven al planificador de consultas para optimizar—, y nada te impide insertar dos filas con el mismo event_id. Declárala porque documenta la intención y acelera consultas, pero la deduplicación de la que habla Sandra la tienes que hacer tú en el proceso de carga. Si te fías de la PK, los duplicados de Kafka entran igual y tus métricas salen infladas.

## 15:30 - 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:

1Lucía Fernández 15:32
2Ey, Pablo me dijo que estás con los eventos 🚀
3Pregunta rápida: ¿puedes decirme cuántos usuarios ACTIVOS
4tuvimos la semana pasada? Es para una slide de mañana.
5No necesita ser exacto-exacto, un orden de magnitud.
6
7Tú 15:45
8Hola Lucía! Estoy en ello. Una pregunta antes de darte
9el número: cuando dices "activos", ¿te refieres a acciones
10core (crear/completar tareas, comentar) o cualquier uso
11incluyendo solo ver boards?
12
13Lo pregunto porque estamos definiendo la métrica con Sandra
14y queremos asegurarnos de que todos usemos el mismo número.
15
16Lucía Fernández 15:47
17Mmm buena pregunta 🤔
18Para la slide creo que core tiene más sentido. Los que
19DE VERDAD usan el producto, no los que solo miran.
20¿Cuánto te lleva?
21
22Tú 15:49
23Dame 30 min, hago un conteo rápido con la muestra.
24
25Lucía Fernández 15:49
26Eres 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: ~1.200 usuarios core activos la semana pasada, de los 8.200 registrados. 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ás 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 900 en vez de 1.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 1.000 y 1.400"). Lucía es razonable y lo entiende. El problema es cuando alguien DOWNSTREAM copia el número sin el caveat.

## 16:30 - Presentar la definición a Pablo

1#data-team | Martes 16:35
2
3Tú 16:35
4@Pablo @Sandra - Propuesta de definición de "usuario activo":
5
6CORE ACTIVE: usuario que en un período dado ha realizado al
7menos 1 acción con intención:
8- task_created, task_completed, task_assigned
9- comment_added, file_uploaded
10- board_created, automation_created
11
12BROAD ACTIVE: core + acciones de consumo:
13- board_viewed, task_viewed, search_performed
14- filter_applied, notification_clicked
15- dashboard_viewed, report_exported
16
17SYSTEM: no cuenta como actividad
18- login, session_started/ended, emails, push, billing
19
20Para retención usamos CORE. Para engagement general, BROAD.
21Sandra está de acuerdo. Lucía también (le pregunté).
22
23Pablo Herrera 16:42
24👍 Perfecto. Me gusta que sea una definición dual.
25Documéntalo en el Notion de métricas y a partir de ahora
26esa es la fuente de verdad.
27Una cosa: añade la fecha de vigencia. "Desde 2024-10-08,
28esta es la definición oficial." Así si en el futuro
29cambiamos, queda histórico.
30
31Sandra Vidal 16:44
32+1 📊 Ya lo enlazo desde el schema.yml de dbt.
33Bien hecho!
34
35 ✅ 2 📋 1

Regístrate para guardar tu progreso.