Saltar al contenido

lección 7

Día 5 -- A/B testing y la presentación a Meridian Capital

Diseñas el framework de A/B testing con significancia estadística. Acompañas a Marta a la presentación y el dashboard cuenta la historia correcta.

⏱ 55 min

## Viernes, 7:30 AM -- El día de la verdad

El framework de A/B testing de hoy funciona sobre parámetros (baseline, efecto mínimo detectable, DAU) y no lee ficheros directamente. Esos parámetros salen de las métricas que calculaste antes con `datos/reproducciones_eventos.parquet` y `datos/suscripciones.csv`: el baseline de completion (~55%) y el DAU (~2100) son las entradas que usarás. Si quieres recalcularlos para tu propio experimento, parte de esos ficheros guardados en `datos/`.

Hoy es viernes. A las 11:00 Marta se sienta frente a tres partners de Meridian Capital con tu dashboard proyectado en la pantalla. Pero antes de eso, tienes 3 horas para completar la última pieza del puzzle: un framework de A/B testing que demuestre a los inversores que StreamVibe no solo mide -- sino que experimenta de forma rigurosa para mejorar.

Los VCs sofisticados ya no se conforman con métricas bonitas. Quieren ver que el equipo tiene una cultura de experimentación: "¿Cómo sabéis que las mejoras son reales y no ruido estadístico?" Si puedes mostrar un framework de A/B testing funcionando -- aunque sea con un solo experimento -- demuestras madurez analítica que poca startup pre-Serie A tiene.

## ¿Por qué A/B testing en una startup de 15K usuarios?

La objeción habitual es "somos muy pequeños para hacer A/B tests". Es falsa. Con 15K MAU y ~2.100 DAU, puedes hacer tests que alcanzan significancia estadística en 7-14 días para efectos del 10% o más. No es Netflix con millones de usuarios donde puedes detectar mejoras del 0.5% -- pero sí puedes validar decisiones grandes como "¿el onboarding nuevo retiene más?" o "¿el autoplay entre episodios aumenta el watch time?"

La razón estratégica para tener A/B testing desde el día 1 es cultural, no técnica. Si el equipo de producto se acostumbra a lanzar features sin validar, cuando tengan 150K usuarios seguirán sin validar -- pero ahora con consecuencias mucho mayores. Establecer la cultura de "medimos antes de decidir" cuando eres pequeño es infinitamente más fácil que implantarla después.

Además, los inversores lo saben. Cuando Meridian Capital pregunta "¿hacéis A/B testing?", no preguntan si podéis detectar efectos del 0.1%. Preguntan: "¿tenéis una cultura de experimentación? ¿tomáis decisiones de producto basadas en evidencia o en opinión del fundador?" Tener un framework funcionando, aunque sea simple, demuestra madurez analítica.

## El framework de A/B testing

El framework tiene 4 componentes: asignación de usuarios a variantes, recolección de métricas por variante, cálculo de significancia estadística y decisión (ship/kill/continue). Lo implementas como un módulo Python que puede correr manualmente o integrase con el pipeline diario.

1# Framework de A/B Testing para StreamVibe
2# Componente 1: Diseño del experimento
3
4from dataclasses import dataclass
5from typing import Optional
6import hashlib
7
8
9@dataclass
10class Experiment:
11 """Definición de un experimento A/B."""
12 id: str
13 name: str
14 description: str
15 metric: str # La métrica principal que mides
16 hypothesis: str # "Si hacemos X, entonces Y aumentará en Z%"
17 min_sample_size: int # Usuarios mínimos por variante
18 min_days: int # Duración mínima
19 variants: list[str] # ['control', 'treatment'] o más
20 traffic_split: list[float] # [0.5, 0.5] para 50/50
21
22
23def assign_user_to_variant(
24 user_id: int,
25 experiment_id: str,
26 variants: list[str],
27 traffic_split: list[float]
28) -> str:
29 """Asigna un usuario a una variante de forma determinística.
30
31 Usa hash(user_id + experiment_id) para que:
32 - El mismo usuario siempre vea la misma variante
33 - La asignación sea uniforme sin base de datos
34 - Diferentes experimentos tengan asignaciones independientes
35 """
36 hash_input = f"{user_id}:{experiment_id}"
37 hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
38 normalized = (hash_value % 10000) / 10000.0 # 0.0 a 1.0
39
40 cumulative = 0.0
41 for i, split in enumerate(traffic_split):
42 cumulative += split
43 if normalized < cumulative:
44 return variants[i]
45
46 return variants[-1] # Fallback al último

Asignación determinística con hash: sin base de datos, sin estado, reproducible.

1# Componente 2: Análisis de significancia estadística
2
3import numpy as np
4from scipy import stats
5from dataclasses import dataclass
6
7
8@dataclass
9class ResultadoAB:
10 """Resultado del analisis de un experimento."""
11 experimento: str
12 media_control: float
13 media_tratamiento: float
14 # La diferencia en las unidades de la metrica, y su intervalo. El intervalo
15 # es lo que de verdad se lee: dice entre que dos valores esta el efecto.
16 diferencia: float
17 ic_bajo: float
18 ic_alto: float
19 lift_relativo: float | None # None si el control es cero
20 p_valor: float
21 # NO hay campo 'confidence_level'. La version anterior guardaba 1 - p_valor
22 # y lo llamaba "nivel de confianza", que es la confusion mas extendida que
23 # existe con los p-valores: un p de 0,30 se informaba como "confianza 70%".
24 # Un p-valor no es la probabilidad de que el cambio funcione (ver el bloque
25 # de teoria de abajo), y ese nombre tiene el formato perfecto para acabar en
26 # una diapositiva y decidir algo.
27 es_significativo: bool
28 n_control: int
29 n_tratamiento: int
30 dias_corridos: int
31 guardarrailes_ok: bool | None
32 decision: str # 'ship', 'kill', 'seguir', 'aun_no'
33 motivo: str
34
35
36def analizar_metrica_continua(
37 df, # una fila por OBSERVACION, con user_id y variant
38 metrica: str,
39 dias_corridos: int,
40 min_dias: int,
41 min_por_rama: int,
42 alpha: float = 0.05,
43) -> ResultadoAB:
44 """Analiza un experimento con reparto POR USUARIO.
45
46 La unidad de analisis tiene que ser la MISMA que la unidad de reparto. Si se
47 reparten personas y se cuentan sesiones, cada persona aporta tantas
48 observaciones como sesiones tenga, el test cree tener mas datos de los que
49 tiene, y el p-valor sale mas pequeno de lo que deberia.
50 """
51 # UNA FILA POR PERSONA. Este groupby es el arreglo mas importante de toda la
52 # leccion: analizando sesiones, entre el 27% y el 55% de los experimentos SIN
53 # NINGUN EFECTO salen significativos, segun cuanto se parezcan entre si las
54 # sesiones de una misma persona. Analizando usuarios sale el 5% que toca.
55 por_usuario = df.groupby(['variant', 'user_id'])[metrica].mean().reset_index()
56 control = por_usuario.loc[por_usuario['variant'] == 'control', metrica].to_numpy()
57 tratamiento = por_usuario.loc[por_usuario['variant'] == 'treatment', metrica].to_numpy()
58
59 # LA FECHA DE CIERRE MANDA. Antes de llegar al minimo de dias Y al minimo de
60 # muestra POR RAMA, no se decide nada -- ni siquiera si el p-valor sale
61 # precioso. Mirar todos los dias y parar cuando sale bien convierte el 5% en
62 # el 21%: cada mirada es otra oportunidad de que el azar cruce el umbral.
63 # Y el minimo se mira POR RAMA, no sobre la suma: un reparto desigual puede
64 # dar 1.400 en total con 300 en una de las dos.
65 n_min = min(len(control), len(tratamiento))
66 if dias_corridos < min_dias or n_min < min_por_rama:
67 return ResultadoAB(
68 experimento='', media_control=0, media_tratamiento=0,
69 diferencia=0, ic_bajo=0, ic_alto=0, lift_relativo=None,
70 p_valor=1.0, es_significativo=False,
71 n_control=len(control), n_tratamiento=len(tratamiento),
72 dias_corridos=dias_corridos, guardarrailes_ok=None,
73 decision='aun_no',
74 motivo=(f'faltan datos: {dias_corridos}/{min_dias} dias, '
75 f'{n_min}/{min_por_rama} usuarios en la rama mas pequena'),
76 )
77
78 m_c, m_t = control.mean(), tratamiento.mean()
79 diferencia = m_t - m_c
80
81 # Intervalo de la DIFERENCIA, en las unidades de la metrica. Es lo que hay
82 # que leer y lo que hay que ensenar: un intervalo que va de -2% a +18% no es
83 # "no hay efecto", es "no lo sabemos todavia".
84 se = np.sqrt(control.var(ddof=1) / len(control)
85 + tratamiento.var(ddof=1) / len(tratamiento))
86 gl = len(control) + len(tratamiento) - 2
87 t_critico = stats.t.ppf(1 - alpha / 2, gl)
88 ic_bajo, ic_alto = diferencia - t_critico * se, diferencia + t_critico * se
89
90 _, p_valor = stats.ttest_ind(control, tratamiento, equal_var=False)
91 es_significativo = bool(p_valor < alpha)
92
93 # El lift relativo NO se puede calcular si el control es cero. Se deja a
94 # nulo; la diferencia en unidades siempre se da.
95 lift = (diferencia / m_c * 100) if m_c != 0 else None
96
97 if not es_significativo:
98 # Y aqui la decision NO es 'kill' automatico. Depende de lo que diga el
99 # intervalo frente al efecto minimo que interesaba: si el intervalo
100 # descarta ese efecto, el cambio no sirve; si todavia lo incluye, el
101 # experimento se ha quedado corto de muestra y eso es otra cosa.
102 decision, motivo = 'kill', 'el intervalo no llega al efecto minimo buscado'
103 elif diferencia > 0:
104 decision, motivo = 'ship', 'mejora significativa'
105 else:
106 decision, motivo = 'kill', 'empeora de forma significativa'
107
108 return ResultadoAB(
109 experimento='', media_control=round(m_c, 3), media_tratamiento=round(m_t, 3),
110 diferencia=round(diferencia, 3),
111 ic_bajo=round(ic_bajo, 3), ic_alto=round(ic_alto, 3),
112 lift_relativo=round(lift, 2) if lift is not None else None,
113 p_valor=round(float(p_valor), 4), es_significativo=es_significativo,
114 n_control=len(control), n_tratamiento=len(tratamiento),
115 dias_corridos=dias_corridos, guardarrailes_ok=None,
116 decision=decision, motivo=motivo,
117 )
118
119
120def analyze_conversion_metric(
121 control_conversions: int,
122 control_total: int,
123 treatment_conversions: int,
124 treatment_total: int,
125 dias_corridos: int,
126 min_dias: int,
127 min_por_rama: int,
128 alpha: float = 0.05,
129) -> ResultadoAB:
130 """Analiza una metrica de proporcion (% que completa, % que vuelve).
131
132 Aqui la unidad ya es el usuario: cada persona cuenta una vez, convirtio o no.
133 """
134 if dias_corridos < min_dias or min(control_total, treatment_total) < min_por_rama:
135 return ResultadoAB(
136 experimento='', media_control=0, media_tratamiento=0, diferencia=0,
137 ic_bajo=0, ic_alto=0, lift_relativo=None, p_valor=1.0,
138 es_significativo=False, n_control=control_total,
139 n_tratamiento=treatment_total, dias_corridos=dias_corridos,
140 guardarrailes_ok=None, decision='aun_no',
141 motivo='todavia no se ha llegado al cierre del experimento',
142 )
143
144 tasa_c = control_conversions / control_total
145 tasa_t = treatment_conversions / treatment_total
146 # La diferencia en PUNTOS PORCENTUALES, que es la que siempre se puede dar.
147 diferencia = (tasa_t - tasa_c) * 100
148
149 tabla = np.array([
150 [control_conversions, control_total - control_conversions],
151 [treatment_conversions, treatment_total - treatment_conversions],
152 ])
153
154 # CORTOCIRCUITO ANTES DEL TEST. Con las dos ramas al 0% o las dos al 100%, la
155 # tabla no tiene ninguna variacion y chi-cuadrado revienta al dividir por una
156 # frecuencia esperada de cero. Y el primer dia de cualquier experimento la
157 # tabla esta casi vacia, asi que este caso VA A PASAR.
158 if tabla.min(axis=0).sum() == 0 or (tabla[:, 0].sum() == 0):
159 p_valor = 1.0
160 else:
161 esperadas = stats.contingency.expected_freq(tabla)
162 if esperadas.min() < 5:
163 # Con casillas esperadas por debajo de 5, chi-cuadrado no vale. El
164 # test exacto de Fisher si, y es el caso de los primeros dias.
165 _, p_valor = stats.fisher_exact(tabla)
166 else:
167 _, p_valor, _, _ = stats.chi2_contingency(tabla)
168
169 es_significativo = bool(p_valor < alpha)
170
171 # El lift RELATIVO es imposible si el control convierte cero: seria dividir
172 # por cero. Se deja a nulo y se da la diferencia en puntos, que siempre vale.
173 lift = (diferencia / (tasa_c * 100) * 100) if tasa_c > 0 else None
174
175 # Intervalo de la diferencia de proporciones, en puntos.
176 se = np.sqrt(tasa_c * (1 - tasa_c) / control_total
177 + tasa_t * (1 - tasa_t) / treatment_total) * 100
178 z = stats.norm.ppf(1 - alpha / 2)
179
180 if not es_significativo:
181 decision, motivo = 'kill', 'sin diferencia detectable al cierre'
182 elif diferencia > 0:
183 decision, motivo = 'ship', 'mejora significativa'
184 else:
185 decision, motivo = 'kill', 'empeora de forma significativa'
186
187 return ResultadoAB(
188 experimento='', media_control=round(tasa_c, 4),
189 media_tratamiento=round(tasa_t, 4), diferencia=round(diferencia, 2),
190 ic_bajo=round(diferencia - z * se, 2), ic_alto=round(diferencia + z * se, 2),
191 lift_relativo=round(lift, 2) if lift is not None else None,
192 p_valor=round(float(p_valor), 4), es_significativo=es_significativo,
193 n_control=control_total, n_tratamiento=treatment_total,
194 dias_corridos=dias_corridos, guardarrailes_ok=None,
195 decision=decision, motivo=motivo,
196 )
197
198
199def revisar_guardarrailes(guardarrailes: list[dict]) -> tuple[bool, list[str]]:
200 """Comprueba los guardarrailes ANTES de mirar la metrica principal.
201
202 Cada guardarrail lleva su DIRECCION: la tasa de cancelacion se rompe si
203 SUBE, el DAU global si BAJA. Un guardarrail sin direccion no se puede
204 comprobar, y en la version anterior se disenaban con cuidado en el ejercicio
205 y despues ninguna funcion los miraba.
206 """
207 roto = []
208 for g in guardarrailes:
209 # Se compara el LIMITE del intervalo, no la estimacion suelta: si no, se
210 # mata un experimento cada vez que el azar mueve un guardarrail.
211 if g['direccion'] == 'no_subir' and g['ic_bajo'] > g['limite']:
212 roto.append(f"{g['nombre']}: sube por encima de {g['limite']}")
213 elif g['direccion'] == 'no_bajar' and g['ic_alto'] < g['limite']:
214 roto.append(f"{g['nombre']}: baja por debajo de {g['limite']}")
215 return (len(roto) == 0), roto

El analizador, con la unidad correcta, fecha de cierre y los guardarraíles que sí se miran.

## Qué es y qué no es un p-valor

Merece la pena pararse aquí, porque el campo que acabamos de borrar del resultado no era un descuido de programación: era una creencia, y es la más extendida que hay sobre esto. Un p-valor de 0,03 NO significa que haya un 97% de probabilidades de que el cambio funcione. Significa otra cosa, más estrecha y menos satisfactoria: si el cambio no hiciera absolutamente nada, un resultado así o más extremo aparecería tres veces de cada cien.

La diferencia no es filosófica, cambia decisiones. El p-valor te dice qué probabilidad tienen tus datos suponiendo que el cambio no sirve. Lo que quieres saber es lo contrario: qué probabilidad tiene el cambio de servir, dados tus datos. Y para pasar de lo primero a lo segundo hace falta algo más -- cuántos de tus experimentos anteriores funcionaron, por ejemplo. Si de cada diez cosas que pruebas funciona una, la mayoría de tus resultados "significativos" van a ser falsos por mucho que el p-valor sea pequeño.

  • Un p-valor alto no demuestra que el cambio no sirva. Demuestra que con estos datos no se distingue de cero, y eso incluye el caso "no hay suficientes datos todavía".
  • Un p-valor pequeño no dice que el efecto sea grande. Con muchísima muestra, una mejora irrelevante sale con un p-valor diminuto. Para saber si importa hay que mirar el tamaño del efecto, no el p-valor.
  • Y un p-valor no se interpreta sin saber cuántas veces has mirado. Ése es el punto siguiente, y es el que más dinero cuesta.

La costumbre que resuelve casi todo esto: informa del intervalo del efecto y no del p-valor. "La mejora está entre 1 y 19 puntos" se puede leer sin haber estudiado estadística, y contesta las dos preguntas que importan -- ¿hay efecto? ¿es lo bastante grande para que merezca la pena? Un intervalo que va de -2 a +18 no es "no hay efecto": es "no lo sabemos todavía", que es una conclusión distinta y mucho más honesta que un "no significativo".

## Mirar muchas veces es mirar con otro umbral

El 5% es por comprobación, no por experimento. Cada vez que mires el p-valor le das otra oportunidad al azar de cruzar el umbral, y si la costumbre es mirar todos los días y parar cuando sale bien, el umbral real no es el 5%: es mucho peor. Esto no es una advertencia teórica, se puede medir. Cuatro mil experimentos simulados en los que el cambio no hace absolutamente nada:

1# La comprobacion que deberia llevar cualquier marco de experimentacion, y que
2# casi ninguno lleva: probarlo contra experimentos donde SE SABE que no hay
3# efecto. Si el marco es honesto, tiene que equivocarse el 5% de las veces.
4import numpy as np
5from scipy import stats
6
7
8def tanda_sin_efecto(n_exp=4000, dias=14, usuarios_dia=60, mirando_cada_dia=True):
9 rs = np.random.default_rng(7)
10 significativos = 0
11 ships = 0
12 dias_del_cruce = []
13 for _ in range(n_exp):
14 control, tratamiento = [], []
15 cruzo = False
16 for d in range(dias):
17 # EL MISMO proceso en las dos ramas. No hay efecto que encontrar.
18 control.extend(rs.lognormal(6.5, 0.9, usuarios_dia))
19 tratamiento.extend(rs.lognormal(6.5, 0.9, usuarios_dia))
20 if mirando_cada_dia:
21 _, p = stats.ttest_ind(control, tratamiento, equal_var=False)
22 if p < 0.05 and not cruzo:
23 cruzo = True
24 dias_del_cruce.append(d + 1)
25 if np.mean(tratamiento) > np.mean(control):
26 ships += 1
27 if not mirando_cada_dia:
28 _, p = stats.ttest_ind(control, tratamiento, equal_var=False)
29 cruzo = p < 0.05
30 if cruzo:
31 significativos += 1
32 return {
33 'significativos_pct': round(100 * significativos / n_exp, 1),
34 'ship_pct': round(100 * ships / n_exp, 1),
35 'dia_mediano_del_cruce': int(np.median(dias_del_cruce)) if dias_del_cruce else None,
36 }
37
38
39print(tanda_sin_efecto(mirando_cada_dia=False))
40# {'significativos_pct': 4.8, 'ship_pct': 0.0, 'dia_mediano_del_cruce': None}
41
42print(tanda_sin_efecto(mirando_cada_dia=True))
43# {'significativos_pct': 20.8, 'ship_pct': 11.2, 'dia_mediano_del_cruce': 4}

Cuatro mil experimentos donde no hay nada que encontrar. Mirando cada día, uno de cada cinco "encuentra" algo.

## La unidad de análisis es la unidad de reparto

Éste es el error más caro de los dos y el que menos se ve, porque no hay nada raro en el código: se reparten personas, se analizan sesiones, y todo parece correcto. Pero el p-valor vive de un supuesto -- que cada dato es independiente de los demás -- y las doce sesiones de una misma persona no lo son. Se parecen entre sí: quien ve media hora suele ver media hora, y quien ve tres horas suele ver tres horas.

El efecto es que el test cree tener doce veces más datos de los que tiene. Y cuánto se equivoca depende de una sola cosa: de cuánto se parezcan entre sí las sesiones de una persona. Dos mil experimentos simulados, quinientos usuarios por rama, doce sesiones cada uno, y sin ningún efecto del experimento:

1# Cuanta variacion esta ENTRE personas (el resto esta dentro de cada persona).
2#
3# variacion entre personas analizando SESIONES analizando USUARIOS
4# 20 % 26,6 % 4,8 %
5# 40 % 41,6 % 5,0 %
6# 60 % 47,7 % 5,2 %
7# 83 % 55,0 % 4,8 %
8#
9# La columna del medio es el error: entre uno de cada cuatro y uno de cada dos
10# experimentos "encuentra" una diferencia que no existe, y el rango depende de un
11# supuesto sobre los datos que habria que MEDIR, no de la simulacion.
12#
13# La de la derecha es el arreglo: el 5% que toca, en las cuatro filas. Aguanta
14# cualquier supuesto.
15#
16# En consumo de video la variacion entre personas es alta -- unos ven una hora al
17# mes y otros cincuenta -- asi que este caso esta en la parte fea de la tabla.
18
19# Y el arreglo es una linea:
20por_usuario = df.groupby(['variant', 'user_id'])[metrica].mean().reset_index()

Repartir personas y contar sesiones: de uno de cada cuatro a uno de cada dos experimentos miente.

Experimentos donde no hay nada que encontrar. Lo que debería salir es el 5%.

## El primer experimento: autoplay entre episodios

Daniel ya tenía implementado un feature flag para autoplay (reproducir el siguiente episodio automáticamente tras 5 segundos de cuenta atrás). Estaba desactivado porque "no sabíamos si los usuarios lo querían o les molestaba". Perfecto: es tu primer A/B test. Hipótesis: "Si activamos autoplay entre episodios de series, el watch time por sesión aumentará al menos un 15% porque reduce la fricción entre episodios."

Configuras el experimento: 50% de usuarios (asignados por hash) ven autoplay activado, 50% siguen con el comportamiento actual. Métricas que rastrear: watch_time por sesión (primaria), completion_rate de series (secundaria), tasa de skip del autoplay (guardrail -- si > 60% de skips, el feature molesta).

## 10:30 AM -- Preparando la presentación

Marta te pide 10 minutos para repasar el dashboard antes de la reunión. Te sientas con ella en la sala y recorren las 3 pantallas:

  1. 01.Pantalla 1 -- Los números. "1.500 usuarios activos en las tres semanas con datos, 405 el último día. Retención del 73,0% al día siguiente y del 29,4% a la semana." Y debajo, en letra normal: "datos hasta el 13 de octubre, sobre 1.432 y 913 usuarios respectivamente". Marta: "esto es lo que quería. Vuelven."
  2. 02.Pantalla 2 -- Engagement. "Uno de cada cuatro usuarios de los que entran cada mes es un usuario intenso: casi siete días activos de veintiuno y diez horas de contenido." Con la base escrita: de los que entran cada mes, no de los 20.000 registrados.
  3. 03.Pantalla 3 -- Contenido. "El formato episodio es el único al que la gente vuelve: 1,58 sesiones por espectador frente a 1,01 en películas. Por eso el presupuesto de licencias va ahí." Marta: "¿y lo de que se terminan más?". Y le explicas por qué esa cifra no se puede usar para comparar formatos.

Y hay una cuarta pantalla que no existe, y es la que Marta te va a agradecer más el lunes: la de lo que todavía no se puede decir. El stickiness no está, porque con veintiún días de histórico la ventana de treinta no se llena. La tendencia mensual no está, porque no hay dos meses completos que comparar. El 80% de crecimiento orgánico no está, porque al mirarlo es el 35% y el programa de referidos cuesta 60.872 euros. Y el importe de las licencias no está, porque no cuadra con los ingresos por un factor de doce y eso es una pregunta para Javier, no un dato. Cuatro huecos con su motivo escrito. Un inversor que encuentra un hueco explicado piensa "esta gente sabe lo que sabe"; uno que pilla un número inflado deja de creerse los otros cuatro.

Marta te mira y dice: "hace cinco días no teníamos ni un número real. Ahora tengo cuatro cifras que puedo defender y una lista de cuatro que no voy a decir. Lo segundo me da más tranquilidad que lo primero, porque si me preguntan algo que no sé, ya sé que no lo sé." Se levanta, se alisa la chaqueta y se va a la sala de reuniones.

## 11:00 AM -- La presentación a Meridian Capital

No estás en la sala (Marta decidió que era mejor ir sola con Daniel), pero a las 12:15 te llega un mensaje por Slack:

Marta (mensaje, 12:15): "Han pedido ver el panel EN VIVO. Lo primero que ha mirado el mayor de los dos ha sido la línea de la fecha, y ha preguntado por qué el stickiness aparece con un 'disponible el 12 de noviembre'. Se lo he contado tal cual: tres semanas de histórico, la ventana de treinta días no se llena. Ha dicho 'muy bien' y ha pasado a otra cosa. Preguntas: cómo calculamos la retención (ventana de más o menos un día, y que las cohortes jóvenes no entran), qué stack usamos (les ha gustado mucho que sean once euros al mes en vez de cuatrocientos), y si hacemos experimentos. Ahí les he enseñado el marco y han preguntado si corregimos por mirar el resultado varias veces. Les he dicho que sí, que hay fecha de cierre. Se han mirado entre ellos. Nos piden sala de datos para la revisión. Gracias."

A las 14:00 Marta sale de la sala: "no es oficial, pero van a preparar una propuesta la semana que viene. Serie A de cuatro millones. Y te cuento lo que me ha dicho el mayor al salir, porque te va a interesar más que la cifra: que lo que le ha convencido no es el panel, es que le dijera qué no sabemos. Que en la mayoría de las reuniones le enseñan cinco métricas perfectas y él se pasa la hora buscando la que falta."

Cinco días para pasar de cero infraestructura de datos a un panel que ayuda a cerrar una ronda. Pero cuidado con la lectura fácil, porque no es que el panel sea bonito ni que los números sean buenos. Es que son comprobables: cada cifra sale de una consulta que se puede volver a ejecutar delante de quien pregunte, y las que no se sostienen están fuera con su motivo escrito. Eso es lo que aporta un ingeniero de datos con oficio, y es lo contrario de lo que la mayoría de la gente cree que se le pide.

## Lo que viene: la hoja de ruta de datos post-Serie A

  1. 01.Semana 2: Event tracking real en el reproductor (ya no dependemos de la tabla playbacks legacy)
  2. 02.Mes 1: Contratar un analista de datos -- Sofía necesita soporte continuo para decisiones de contenido
  3. 03.Mes 2: Migrar cron -> Airflow cuando tengamos > 10 pipelines
  4. 04.Mes 3: Implementar el A/B testing como parte del ciclo de producto (1 test por sprint)
  5. 05.Mes 4: Evaluar DuckDB -> Redshift Serverless si el volumen lo justifica
  6. 06.Mes 6: Recomendaciones v2 con modelo de embeddings (si contratamos ML engineer)

## Resumen del Día 5

  • Montaste el marco de experimentación con reparto determinista por hash, y analizando por la MISMA unidad con la que se reparte: una fila por persona, no por sesión
  • Comprobaste el marco contra experimentos donde sabías que no había nada que encontrar, que es la única forma de saber que un marco funciona
  • Y encontraste dos errores que valen por toda la lección: mirar cada día convierte el 5% en el 20,8%, y contar sesiones repartiendo personas lo lleva hasta el 55%
  • Pusiste fecha de cierre: la función no devuelve decisión hasta llegar al mínimo de días y de muestra por rama, ni aunque el p-valor salga precioso
  • Quitaste el campo que llamaba "nivel de confianza" a uno menos el p-valor, y lo sustituiste por el intervalo del efecto, que es lo que de verdad se lee
  • Hiciste que los guardarraíles se comprueben antes de la métrica principal, cada uno con su dirección, y comparando el límite del intervalo para no matar experimentos por azar
  • Protegiste el análisis de los casos que van a pasar el primer día: tasas al 0%, al 100% y casillas esperadas por debajo de cinco
  • Preparaste a Marta con cuatro cifras que puede defender y una lista de cuatro que no va a decir, cada una con su motivo
  • Y en cinco días pasaste de cero infraestructura a un panel que ayuda a cerrar una ronda -- no porque sea bonito, sino porque cada número se puede volver a ejecutar delante de quien pregunte

Regístrate para guardar tu progreso.