lección 5
Día 3 (tarde) -- Modelo analítico de contenido
Respondes la pregunta de Sofía: ¿qué contenido funciona y por qué? Analizas engagement por género, duración y tipo. Diseñas el content performance score.
⏱ 55 min
## Miércoles, 14:00 -- La pregunta de Sofía
Esta tarde se cruzan dos fuentes que no casan. Por un lado `catalogo.csv`, con `content_id, title, content_type, genre_primary, duration_minutes, season_count, episode_count, is_original` -- y ojo con la unidad, porque la duración está en MINUTOS y el tiempo visto en segundos, así que cualquier cociente entre los dos necesita un por 60. Por otro, los costes de licencia, que no están en el catálogo: viven en `costes_licencias.csv` (el mismo contenido que el `.xlsx` que mantiene Javier a mano, para que se pueda consultar con SQL). Ese fichero tiene 388 filas para 1.200 títulos, los importes en formato europeo, las fechas en `DD/MM/YYYY`, los títulos con erratas y ocho packs que agrupan varios títulos sin decir cuáles. Lo único que comparten las dos fuentes es el título, y casarlas es la mitad del trabajo de la tarde.
Después de comer, Sofía te pilla en la cocina: "He oído que ya tienes datos funcionando. ¿Puedes ayudarme con algo? Tengo que decidir en qué gastar 200K EUR en licencias de contenido para el Q2 y literalmente no tengo ni idea de qué funciona mejor en nuestra plataforma. ¿Comedia o drama? ¿Series cortas o películas largas? ¿Documentales de naturaleza o true crime? Cualquier dato que me des es mejor que lo que tengo ahora, que es mi intuición."
Esta es la pregunta de negocio más interesante de la semana. Para Marta, las métricas de producto cuentan una historia de tracción. Para Sofía, el análisis de contenido responde una pregunta operativa concreta: dónde invertir 200K EUR. Si le das la respuesta correcta, StreamVibe compra el contenido que maximiza engagement y retención. Si se equivoca, quema presupuesto en títulos que nadie ve.
## El modelo de performance de contenido
Necesitas un framework que evalúe cada pieza de contenido en múltiples dimensiones. No basta con "cuántas veces se vio" -- una película mala puede tener muchos plays (por curiosidad o marketing) pero poca retención. El verdadero indicador de que un contenido funciona es: ¿la gente lo termina? ¿vuelve al día siguiente? ¿lo recomienda (proxied por si otros usuarios con patrones similares también lo ven)?
1-- Lo primero es el coste por titulo, porque no viene en el catalogo.2-- Sale de la hoja que mantiene Javier a mano, y hay que limpiarla: importes en3-- formato europeo, sufijos en los titulos, mayusculas y espacios de sobra.4CREATE OR REPLACE VIEW coste_titulo AS5WITH limpio AS (6 SELECT TRIM(REPLACE(titulo, ' (temp 1-2)', '')) AS titulo,7 -- "1.565,32" -> 1565.32: primero fuera el punto de los miles, y8 -- despues la coma decimal a punto. En ese orden, o se pierde todo.9 TRY_CAST(REPLACE(REPLACE(coste_mensual_eur, '.', ''), ',', '.')10 AS DOUBLE) AS coste_eur11 FROM costes_licencias12 -- Los packs fuera de aqui: una fila que dice "Pack series BBC, incluye 513 -- titulos" no tiene un coste por titulo hasta que alguien decida como14 -- repartirlo, y eso es una decision, no un calculo. Se tratan aparte.15 WHERE titulo NOT LIKE 'Pack %'16),17por_clave AS (18 SELECT LOWER(TRIM(titulo)) AS clave, MAX(coste_eur) AS coste_eur19 FROM limpio GROUP BY 120)21SELECT c.content_id, p.coste_eur22FROM content c JOIN por_clave p ON LOWER(c.title) = p.clave;2324-- Y ahora la puntuacion de rendimiento por titulo.25WITH hoy AS (26 -- "Hoy" es el ultimo dia con datos, nunca CURRENT_DATE: el extracto esta27 -- congelado en octubre de 2024, y anclado al reloj del ordenador esta28 -- consulta devuelve cero filas y parece que la escribiste mal.29 SELECT MAX(started_at) AS fecha FROM playbacks30),31por_titulo AS (32 SELECT33 c.content_id,34 c.title,35 c.content_type,36 c.genre_primary,37 c.duration_minutes,38 t.coste_eur,39 COUNT(p.playback_id) AS reproducciones,40 COUNT(DISTINCT p.user_id) AS espectadores,41 SUM(p.watch_seconds) / 3600.0 AS horas,42 -- El cociente de tiempos necesita la duracion EN SEGUNDOS. El catalogo43 -- la da en minutos: sin el x60 el porcentaje sale 60 veces mas alto.44 AVG(p.watch_seconds * 1.045 / NULLIF(c.duration_minutes * 60.0, 0)) * 100 AS pct_visto,46 -- Denominador COUNT(p.playback_id), no COUNT(*): con COUNT(*) la fila47 -- de un titulo que nadie ha visto cuenta 1 y sale un 0% de48 -- finalizacion, que se lee como "nadie lo termina" cuando lo que pasa49 -- es que nadie lo ha empezado.50 SUM(CASE WHEN p.completed THEN 1 ELSE 0 END) * 100.051 / NULLIF(COUNT(p.playback_id), 0) AS pct_terminado52 FROM content c53 LEFT JOIN coste_titulo t ON t.content_id = c.content_id54 -- LAS CONDICIONES SOBRE LA TABLA DE LA DERECHA VAN EN EL ON.55 -- Si la fecha se pone en el WHERE, el cruce por la izquierda deja de serlo:56 -- los titulos sin reproducciones tienen NULL ahi, el NULL no cumple la57 -- condicion y la fila desaparece sin avisar.58 LEFT JOIN playbacks p59 ON c.content_id = p.content_id60 AND p.started_at >= (SELECT fecha FROM hoy) - INTERVAL '90 days'61 AND p.is_meaningful62 GROUP BY 1, 2, 3, 4, 5, 663),64puntuada AS (65 SELECT66 pt.*,67 pt.espectadores * 1.0 / (SELECT COUNT(DISTINCT user_id) FROM playbacks68 WHERE is_meaningful) AS alcance,69 -- Sin coste no hay retorno que calcular. Se deja a NULO, no a cero:70 -- un cero en un factor que pesa el 30% manda el titulo al fondo de la71 -- tabla, y la tabla se usa para decidir que no renovar. Un hueco en la72 -- hoja de Javier no puede convertirse en una decision de no comprar.73 CASE WHEN pt.coste_eur > 074 THEN pt.horas / pt.coste_eur * 1000 END AS horas_por_1000eur75 FROM por_titulo pt76 WHERE pt.reproducciones >= 10 -- por debajo, el porcentaje es ruido77)78SELECT79 content_id, title, content_type, genre_primary, duration_minutes,80 espectadores,81 ROUND(pct_visto, 1) AS pct_visto,82 ROUND(pct_terminado, 1) AS pct_terminado,83 ROUND(horas, 1) AS horas,84 ROUND(horas_por_1000eur, 2) AS horas_por_1000eur,85 -- Sin los tres factores no hay nota. Inventarle una con los dos que si86 -- tenemos tampoco vale: le daria una posicion en el ranking que no se ha87 -- ganado, y como nunca podria salir al final, quedaria protegido de la88 -- lista de no renovar. Las dos cosas son decisiones disfrazadas de calculo.89 CASE WHEN horas_por_1000eur IS NULL THEN NULL ELSE ROUND(90 LEAST(alcance / 0.05, 1.0) * 3091 + LEAST(pct_terminado / 80.0, 1.0) * 4092 + LEAST(horas_por_1000eur / 6.0, 1.0) * 3093 , 1) END AS puntuacion94FROM puntuada95ORDER BY puntuacion DESC NULLS LAST96LIMIT 50;
La puntuación por título: alcance (30%), finalización (40%) y retorno por euro (30%).
## La otra mitad del encargo: lo que no se vuelve a comprar
Repartir 200.000 euros tiene dos caras, y la consulta de arriba sólo enseña una. Ordena lo que se ve lo suficiente para poder juzgarlo, y eso responde a "en qué invertir". La otra pregunta -- "qué dejo de pagar" -- son justo los títulos que el umbral de diez reproducciones acaba de echar de la tabla, y son 929 de 1.200. Esa lista no existe en ninguna parte y es la que Sofía va a agradecer más, porque el dinero que libera es dinero que puede gastar.
1-- Lo que no se vuelve a comprar: licencia pagada y casi nadie viendolo.2WITH hoy AS (SELECT MAX(started_at) AS fecha FROM playbacks)3SELECT4 c.content_id, c.title, c.genre_primary, c.duration_minutes,5 t.coste_eur,6 -- Marcado a proposito: un titulo sin coste registrado NO es un titulo7 -- gratis. Es un titulo del que no sabemos lo que pagamos.8 t.coste_eur IS NULL AS coste_sin_registrar,9 COUNT(p.playback_id) AS reproducciones,10 COUNT(DISTINCT p.user_id) AS espectadores11FROM content c12LEFT JOIN coste_titulo t ON t.content_id = c.content_id13LEFT JOIN playbacks p14 ON c.content_id = p.content_id15 AND p.started_at >= (SELECT fecha FROM hoy) - INTERVAL '90 days'16 AND p.is_meaningful17GROUP BY 1, 2, 3, 4, 5, 618HAVING COUNT(p.playback_id) < 1019-- NULLS LAST a proposito: los titulos sin coste no deben encabezar una lista20-- ordenada por coste. Van marcados y al final, no arriba.21ORDER BY t.coste_eur DESC NULLS LAST;2223-- Lo que sale con los datos del caso:24-- 929 titulos con menos de 10 reproducciones en la ventana.25-- De esos, 636 sin coste registrado.26-- Los 293 que si tienen coste suman 764.666 EUR al mes.
La lista que libera presupuesto. 929 títulos, y 764.666 EUR al mes en los que sí tienen coste.
Y aquí aparece algo que hay que mirar antes de seguir. Los 293 títulos poco vistos suman 764.666 euros al mes, y el catálogo entero con coste registrado suma 1.016.918. StreamVibe ingresa unos 87.100 euros al mes. Las licencias cuestan doce veces los ingresos. Eso no es un hallazgo de negocio: es una comprobación de orden de magnitud que falla, y lo que se hace con ella no es construir un análisis encima, es levantarse e ir a preguntar. ¿La columna es mensual o anual? ¿El importe es por título o por acuerdo? ¿Incluye lo que ya no está activo? Hasta que Javier no lo aclare, la cifra absoluta no se puede poner en ninguna diapositiva.
Lo que sí se sostiene mientras tanto es el ORDEN. Si toda la columna está mal por el mismo factor -- porque son euros al año y no al mes, por ejemplo -- los cocientes entre géneros y entre formatos no cambian: un factor constante se va arriba y abajo de la división. Así que el análisis de esta tarde se puede hacer y la recomendación se puede defender; lo que no se puede decir es "cuesta tanto". Saber qué parte de tu resultado sobrevive a un error de escala y qué parte no es una de esas cosas que separan a quien lleva tiempo en esto.
## Análisis por género -- ¿comedia o drama?
1-- La version que parece bien y esta mal. Merece la pena escribirla primero,2-- porque es la que sale sola y el error no da ningun aviso.3SELECT4 c.genre_primary,5 ROUND(SUM(p.watch_seconds) / 3600.06 / SUM(t.coste_eur) * 1000, 3) AS horas_por_1000eur7FROM content c8JOIN playbacks p ON c.content_id = p.content_id9JOIN coste_titulo t ON t.content_id = c.content_id10GROUP BY 111ORDER BY horas_por_1000eur DESC;1213-- documentary 0,371 <-- primera14-- animation 0,37015-- drama 0,35116-- comedy 0,34117-- thriller 0,32018-- horror 0,29419-- romance 0,28120-- sci-fi 0,271 <-- ultima21--22-- Ocho generos entre 0,271 y 0,371. Sospechosamente parecidos.2324-- EL PROBLEMA: despues del cruce hay una fila por REPRODUCCION, y el coste del25-- titulo va repetido en todas. Asi que SUM(t.coste_eur) no es lo que costo el26-- catalogo del genero: es el precio de cada titulo multiplicado por sus veces27-- vistas, y sumado. Un titulo visto cinco mil veces aporta cinco mil veces su28-- precio.29--30-- El efecto no es que el resultado quede raro: es que la metrica se vuelve CIEGA31-- A LA POPULARIDAD. Numerador y denominador crecen los dos con las32-- reproducciones, asi que se cancelan y lo que queda es, en la practica, las33-- horas por reproduccion divididas por el coste medio por reproduccion.3435-- LA VERSION BUENA: una fila por titulo primero, el genero despues.36WITH hoy AS (SELECT MAX(started_at) AS fecha FROM playbacks),37por_titulo AS (38 SELECT39 c.genre_primary AS genero,40 c.content_id,41 -- MAX y no SUM: el coste del titulo es uno, aunque la fila se repita.42 MAX(t.coste_eur) AS coste_eur,43 COALESCE(SUM(p.watch_seconds), 0) / 3600.0 AS horas,44 COUNT(p.playback_id) AS reproducciones,45 COUNT(DISTINCT p.user_id) AS espectadores46 FROM content c47 JOIN coste_titulo t ON t.content_id = c.content_id48 -- Cruce por la IZQUIERDA y con la fecha en el ON: el coste del catalogo que49 -- nadie ha visto tiene que seguir en el denominador. Si se cae, un genero con50 -- mucho catalogo muerto sale mejor cuanto mas muerto este.51 LEFT JOIN playbacks p52 ON c.content_id = p.content_id53 AND p.started_at >= (SELECT fecha FROM hoy) - INTERVAL '90 days'54 AND p.is_meaningful55 GROUP BY 1, 256)57SELECT58 genero,59 COUNT(*) AS titulos,60 SUM(reproducciones) AS reproducciones,61 ROUND(SUM(horas), 1) AS horas,62 ROUND(SUM(horas) / NULLIF(SUM(coste_eur), 0) * 1000, 2) AS horas_por_1000eur63FROM por_titulo64GROUP BY genero65HAVING SUM(reproducciones) >= 10066ORDER BY horas_por_1000eur DESC;6768-- thriller 5,64 <-- primera69-- horror 5,3370-- drama 5,1471-- comedy 5,0772-- romance 4,8173-- documentary 4,2674-- sci-fi 3,6475-- animation 1,78 <-- ultima
La misma pregunta con las dos fórmulas. La mala no sólo aplana el ranking: lo invierte.
Ojo con el cruce por la izquierda de la versión buena, que es igual de importante y se ve menos. El coste del catálogo que nadie ha visto tiene que seguir contando en el denominador, porque se paga igual. Si se usa un cruce normal, los títulos con cero reproducciones desaparecen y un género sale mejor cuanto más catálogo muerto tenga -- lo cual es exactamente al revés de lo que le interesa a Sofía. Es la misma trampa del `ON` contra el `WHERE`, con otra cara: aquí no se pierde una fila por descuido, se pierde el dinero que esa fila representa.
## Sumar una columna después de un cruce casi siempre está mal
El error del retorno por género no es un despiste de esta lección: es uno de los tres o cuatro errores que se repiten toda una carrera, y no da ningún síntoma. Merece la pena quedarse con la forma, porque la vas a reconocer en sitios donde no estás pensando en licencias de contenido.
La forma es ésta. Tienes una tabla de COSAS -- títulos, clientes, productos, pedidos -- y una tabla de SUCESOS -- reproducciones, visitas, líneas de pedido, pagos. Las cruzas. El resultado tiene una fila por suceso, y todas las columnas de la cosa vienen repetidas en cada una de sus filas. En ese momento, `SUM` sobre una columna de la cosa deja de significar lo que su nombre dice: suma el valor una vez por suceso.
- El coste de un título por el número de veces que se vio. Es el de esta lección.
- El precio de un producto por el número de líneas de pedido en que aparece, cuando lo que querías era el valor del inventario.
- El límite de crédito de un cliente por su número de transacciones, cuando lo que querías era la exposición total del banco.
- La superficie de una tienda por su número de ventas, cuando lo que querías era metros cuadrados por región.
La prueba cabe en una frase: di en voz alta lo que crees que estás sumando. Si lo que sale es una propiedad de la cosa -- "lo que costó el catálogo", "el valor del inventario", "los metros de la región" -- hay que agrupar primero por la cosa y sumar después. Si lo que sale es un total ponderado por los sucesos -- "los minutos de contenido servidos", "los euros facturados" -- entonces sumar tras el cruce es exactamente lo correcto. Las dos cosas se escriben casi igual y significan cosas distintas, y ninguna de las dos da error.
Y la señal de alarma que conviene aprender a ver: cuando una métrica que debería variar mucho sale casi igual en todos los grupos, sospecha de un cruce. Ocho géneros entre 0,271 y 0,371 no es un hallazgo de que todos los géneros rinden parecido: es el síntoma de que el numerador y el denominador están creciendo los dos con el mismo factor y se cancelan. Un rango sospechosamente estrecho es una de las pocas pistas gratis que dan estos errores.
## ¿Qué género hace que la gente vuelva?
Ésta es la pregunta buena, porque las horas por euro miden eficiencia y la vuelta mide si el contenido engancha. La forma obvia de medirla -- "de los que vieron este género, cuántos vieron algo en los siete días siguientes" -- tiene un problema que no se ve hasta que la pones al lado de su línea de base: eso no mide el género, mide lo activo que ya era el usuario. Alguien que entra todos los días sale como retenido en los cinco géneros que miró esa semana y los sube todos.
1-- Dos columnas con la MISMA forma, que es lo que hace la comparacion honesta:2-- el mismo conjunto de usuarios y la misma clase de ventana de 7 dias.3-- la primera cuenta las ventanas que arrancan tras ver ESTE genero4-- la segunda, las que arrancan tras CUALQUIER reproduccion de esos mismos5-- usuarios -- o sea, la linea de base de lo activos que ya son6WITH actividad AS (7 SELECT DISTINCT user_id, event_date FROM playbacks WHERE is_meaningful8),9vistas AS (10 SELECT p.user_id, c.genre_primary AS genero, p.event_date11 FROM playbacks p JOIN content c USING (content_id)12 WHERE p.is_meaningful13),14tras_genero AS (15 SELECT v.genero,16 COUNT(DISTINCT v.user_id) AS usuarios,17 COUNT(DISTINCT CASE WHEN EXISTS (18 SELECT 1 FROM actividad a19 WHERE a.user_id = v.user_id20 AND a.event_date BETWEEN v.event_date + 1 AND v.event_date + 721 ) THEN v.user_id END) AS vuelven22 FROM vistas v GROUP BY 123),24linea_base AS (25 -- Los MISMOS usuarios, pero la ventana arranca tras cualquier cosa que vieran.26 SELECT v.genero,27 COUNT(DISTINCT p.user_id) AS usuarios,28 COUNT(DISTINCT CASE WHEN EXISTS (29 SELECT 1 FROM actividad a30 WHERE a.user_id = p.user_id31 AND a.event_date BETWEEN p.event_date + 1 AND p.event_date + 732 ) THEN p.user_id END) AS vuelven33 FROM (SELECT DISTINCT genero, user_id FROM vistas) v34 JOIN playbacks p ON p.user_id = v.user_id AND p.is_meaningful35 GROUP BY 136)37SELECT g.genero, g.usuarios,38 ROUND(g.vuelven * 100.0 / g.usuarios, 1) AS tras_este_genero,39 ROUND(b.vuelven * 100.0 / b.usuarios, 1) AS tras_cualquier_cosa,40 ROUND(g.vuelven * 100.0 / g.usuarios41 - b.vuelven * 100.0 / b.usuarios, 1) AS diferencia42FROM tras_genero g JOIN linea_base b USING (genero)43ORDER BY tras_este_genero DESC;4445-- genero usuarios tras_este_genero tras_cualquier_cosa diferencia46-- drama 1397 74,3 80,5 -6,247-- documentary 1262 70,1 80,3 -10,148-- thriller 1146 68,1 79,8 -11,849-- comedy 1095 67,9 79,5 -11,650-- sci-fi 922 66,4 80,7 -14,351-- romance 538 61,0 82,0 -21,052-- horror 562 61,0 79,9 -18,953-- animation 341 60,7 82,1 -21,4
La misma métrica y su línea de base. La columna de la diferencia es la única que dice algo.
Un detalle de la versión original que conviene ver, porque es el tipo de línea que se cuela en una revisión. La consulta antigua llevaba `AND p2.content_id != p.content_id`, o sea que para contar como "vuelta" tenía que ser a un título DISTINTO. Eso excluye justo el caso que sostiene la conclusión de la lección: seguir viendo la misma serie. En estos datos el `content_id` es el del título, no el del episodio, así que ver el episodio siguiente es el mismo identificador -- y la métrica que iba a demostrar que las series funcionan estaba descartando la prueba. Quitarlo es lo correcto, pero hay que decir lo que pasa al quitarlo: entonces la métrica favorece mecánicamente a lo serializado, porque en una serie el episodio siguiente es una vuelta casi garantizada. Las dos versiones tienen sesgo, en direcciones contrarias. La forma honesta de presentarlo es con las dos columnas separadas: vuelve a lo mismo y vuelve a otra cosa.
## Análisis por duración -- ¿series cortas o películas largas?
1-- Rendimiento por franja de duracion. Mismo patron que el genero: primero el2-- titulo, despues la franja.3--4-- Y OJO CON LOS CORTES. El catalogo da la duracion en MINUTOS. Si se copian los5-- cortes en segundos y solo se cambia el nombre de la columna, TODOS los titulos6-- caen en el primer tramo -- una pelicula de 140 minutos es menor que 1.200 -- la7-- tabla sale con una fila y el analisis desaparece sin dar ningun error.8WITH hoy AS (SELECT MAX(started_at) AS fecha FROM playbacks),9por_titulo AS (10 SELECT11 CASE12 WHEN c.duration_minutes < 20 THEN '< 20 min (corto)'13 WHEN c.duration_minutes < 45 THEN '20-45 min (episodio)'14 WHEN c.duration_minutes < 90 THEN '45-90 min (medio)'15 WHEN c.duration_minutes < 120 THEN '90-120 min (pelicula)'16 ELSE '> 120 min (largo)'17 END AS franja,18 c.content_id,19 MAX(t.coste_eur) AS coste_eur,20 COALESCE(SUM(p.watch_seconds), 0) / 3600.0 AS horas,21 COUNT(p.playback_id) AS reproducciones,22 COUNT(DISTINCT p.user_id) AS espectadores,23 -- Porcentaje visto medio de ESTE titulo. El x60 convierte la duracion a24 -- segundos; sin el, el ratio sale 60 veces mas alto.25 AVG(p.watch_seconds * 1.026 / NULLIF(c.duration_minutes * 60.0, 0)) * 100 AS pct_visto27 FROM content c28 JOIN coste_titulo t ON t.content_id = c.content_id29 LEFT JOIN playbacks p30 ON c.content_id = p.content_id31 AND p.started_at >= (SELECT fecha FROM hoy) - INTERVAL '90 days'32 AND p.is_meaningful33 GROUP BY 1, 234)35SELECT36 franja,37 COUNT(*) AS titulos,38 SUM(reproducciones) AS reproducciones,39 -- Empuja hacia LO CORTO: la duracion esta en el denominador.40 ROUND(AVG(pct_visto), 1) AS pct_visto,41 -- Empuja hacia LO LARGO: las horas vistas crecen con el metraje.42 ROUND(SUM(horas) / NULLIF(SUM(coste_eur), 0) * 1000, 2) AS horas_por_1000eur,43 -- NO depende del metraje. Es el arbitro cuando las dos de arriba se pelean.44 ROUND(SUM(espectadores) / NULLIF(SUM(coste_eur), 0) * 1000, 2)45 AS espectadores_por_1000eur,46 -- Y esta es la que de verdad distingue un formato de otro.47 ROUND(SUM(reproducciones) * 1.0 / NULLIF(SUM(espectadores), 0), 2)48 AS sesiones_por_espectador49FROM por_titulo50GROUP BY franja51HAVING SUM(reproducciones) >= 10052ORDER BY espectadores_por_1000eur DESC;5354-- franja titulos repro pct_visto h/1000EUR esp/1000EUR ses/esp55-- 20-45 min (episodio) 59 1167 90,6 4,20 4,75 1,5856-- > 120 min (largo) 57 719 69,2 6,99 4,65 1,0257-- 90-120 min (pelicula) 78 1052 77,3 5,83 4,39 1,0158-- < 20 min (corto) 27 298 93,2 0,77 4,14 1,0159-- 45-90 min (medio) 159 1907 83,2 4,31 3,89 1,22
Tres métricas y tres sesgos. La cuarta columna es la que contesta la pregunta.
Ninguna de estas tres columnas es la buena, y ése es el punto. El porcentaje visto premia lo corto porque la duración está en el denominador; las horas por euro premian lo largo porque está en el numerador. Enséñalas juntas, di en qué dirección tira cada una, y decide con la que no depende del metraje. Cuando alguien te dé UN solo número para comparar formatos, la pregunta es siempre la misma: hacia dónde tira.
## La presentación a Sofía -- datos que toman decisiones
A las 17:00 te sientas con Sofía 15 minutos y le muestras los resultados. No le enseñas las queries SQL -- le enseñas 3 gráficos simples y una recomendación clara:
La clave de contar datos a alguien que no es técnico, que aprendiste en AdPulse: no enseñes el proceso, enseña el resultado. Sofía no necesita saber que has cruzado tres tablas ni que has tenido que agrupar por título antes de dividir. Necesita saber en qué gastar y qué dejar de pagar. Pero hay una regla que va antes que ésa, y es la que hace que te vuelvan a llamar: cada cifra que digas en voz alta tiene que salir de una consulta que puedas volver a ejecutar delante de ella, con su unidad al lado.
- 01.Formato. El formato episodio (20-45 min) es el único al que la gente vuelve: 1,58 sesiones por espectador frente a 1,01 en películas y en cortos. En eficiencia por euro no gana -- ahí van por delante las películas largas -- y su 90,6% de porcentaje visto no cuenta como argumento, porque esa métrica premia lo corto por construcción. Recomendación: el grueso del presupuesto al formato episodio, y el motivo es la vuelta, no la finalización.
- 02.Género. Con estos datos, ninguno destaca en retención: los ocho quedan por debajo de su propia línea de base, así que la diferencia que parecía haber era la actividad del usuario, no el género. En horas por cada mil euros sí hay orden -- thriller 5,64, drama 5,14, documentales 4,26, animación 1,78 -- y es un orden defendible, pero mide eficiencia, no enganche. Recomendación: usar el género para decidir qué NO renovar, no para decidir qué comprar.
- 03.Lo que libera presupuesto. 929 títulos de 1.200 tienen menos de diez reproducciones en la ventana. De los 293 que además tienen coste registrado, el ahorro está en la tabla, ordenado de más caro a más barato. Ésa es la lista con la que Sofía puede financiar parte del trimestre sin pedir dinero nuevo.
- 04.Lo que no sabemos. De 823 títulos no tenemos coste, así que no tienen nota de rendimiento: aparecen aparte y marcados, no al fondo del ranking. Y la escala de los importes no cuadra -- las licencias suman doce veces los ingresos mensuales -- así que hasta hablar con Javier no hay ninguna cifra absoluta en euros en esta presentación. Los órdenes sí, los importes no.
Sofía se queda con dos cosas y ninguna es la que esperaba. La primera, que el formato importa más que el género, y al revés de como ella lo estaba mirando. La segunda, la lista de los 929: "llevo año y medio renovando esto sin mirar. ¿Me estás diciendo que puedo pagar medio trimestre cancelando cosas que nadie ve?". Y luego la pregunta que vas a oír muchas veces en tu carrera: "¿esto es seguro?". La respuesta honesta es la que le das: el orden sí, los euros no hasta que Javier aclare si su columna es mensual o anual.
Fíjate en lo que NO le has dicho, porque es tan importante como lo que le has dicho. No le has dicho "true crime retiene tres veces más que naturaleza", que es lo que la intuición esperaba y lo que habría salido de la primera versión de estas consultas. No lo has dicho porque al poner la línea de base al lado resultó que no era verdad. Una recomendación de 200.000 euros sostenida en una métrica que medía otra cosa no es una recomendación: es una apuesta con el dinero de otro. Y el día que se caiga, se caerá delante de Marta.
Lo que acabas de hacer con Sofía es lo que separa a un ingeniero de datos senior de uno intermedio, y son dos cosas, no una. La primera: no le has entregado métricas, le has entregado una decisión. "El grueso al formato episodio y aquí están los 929 títulos que puedes cancelar" es infinitamente más útil que un panel con quince gráficos. El senior entrega decisiones, no datos. La segunda, y es la que más cuesta: le has dicho qué parte de tu respuesta no se sostiene todavía. Un intermedio entrega las cuatro conclusiones; un senior entrega tres y una pregunta para Javier.
## El modelo dimensional de contenido
Para que el análisis de contenido sea sostenible (no solo un one-off de está tarde), diseñas un modelo dimensional simple en la zona gold del lake que Metabase pueda consultar directamente:
1-- Tabla gold: rendimiento por titulo. Se recalcula cada noche, despues de que2-- la capa silver este actualizada, y Sofia la consulta cuando quiera sin3-- escribir un solo cruce.4CREATE OR REPLACE TABLE gold_content_performance AS5WITH hoy AS (SELECT MAX(started_at) AS fecha FROM playbacks)6SELECT7 c.content_id,8 c.title,9 c.content_type,10 c.genre_primary,11 c.duration_minutes,12 c.release_year,13 c.is_original,14 t.coste_eur,15 t.coste_eur IS NULL AS coste_sin_registrar,16 COUNT(p.playback_id) AS reproducciones_90d,17 COUNT(DISTINCT p.user_id) AS espectadores_90d,18 ROUND(SUM(p.watch_seconds) / 3600.0, 2) AS horas_90d,19 ROUND(AVG(p.watch_seconds) / 60.0, 1) AS minutos_por_sesion,20 -- Denominador COUNT(p.playback_id) y no COUNT(*): un titulo sin21 -- reproducciones tiene que salir a NULO, no a 0%.22 ROUND(SUM(CASE WHEN p.completed THEN 1 ELSE 0 END) * 100.023 / NULLIF(COUNT(p.playback_id), 0), 1) AS pct_terminado,24 ROUND(AVG(p.watch_seconds * 1.025 / NULLIF(c.duration_minutes * 60.0, 0)) * 100, 1) AS pct_visto,26 -- Sin coste, el retorno se queda a nulo. Un cero seria una afirmacion.27 CASE WHEN t.coste_eur > 028 THEN ROUND(SUM(p.watch_seconds) / 3600.0 / t.coste_eur * 1000, 2)29 END AS horas_por_1000eur,30 -- Cuando se calculo esta foto. Va dentro de la tabla, porque una tabla31 -- materializada sin fecha de calculo es una tabla en la que nadie sabe si32 -- puede confiar.33 CURRENT_TIMESTAMP AS calculada_el34FROM content c35LEFT JOIN coste_titulo t ON t.content_id = c.content_id36LEFT JOIN playbacks p37 ON c.content_id = p.content_id38 AND p.started_at >= (SELECT fecha FROM hoy) - INTERVAL '90 days'39 AND p.is_meaningful40GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9;
La tabla que Sofía consulta sola. Incluye el catálogo entero, también lo que nadie ve.
## Resumen del Día 3 (tarde)
- Sacaste el coste de licencia de donde estaba de verdad -- una hoja hecha a mano, no el catálogo -- y lo casaste por título: importes en formato europeo, sufijos, mayúsculas y ocho packs sin repartir
- Cazaste el error que invertía el ranking de géneros: sumar el coste después del cruce lo multiplica por el número de reproducciones y la métrica se vuelve ciega a la popularidad
- Comparaste formatos con tres métricas y el sesgo de cada una escrito al lado, en vez de con una sola que premiaba lo corto por construcción
- Pusiste una línea de base a la retención por género, y con ella resultó que la diferencia que parecía haber era la actividad del usuario
- Construiste la lista de lo que no se vuelve a comprar: 929 títulos, la mitad del encargo de Sofía, que antes no existía en ninguna parte
- Dejaste sin nota lo que no se puede puntuar, en vez de ponerle un cero que lo habría enterrado por falta de un dato
- Y dijiste en voz alta lo que no se sostiene: la escala de los importes no cuadra con los ingresos, así que el orden se presenta y los euros no
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...
Regístrate para guardar tu progreso.