Variables sintéticas¶
Ejercicio synthetic-variables-annex.pdf · PDF
Compras tres cosas en el supermercado y miras el tique. El precio de cada producto estaba en la estantería. ¿Pero la línea que dice "3 × 1.20 = 3.60", y el TOTAL de abajo? Nadie los pegó en una estantería. Los calculó la caja.
Tus datos son lo mismo. El archivo de los juegos de Steam te dice cuánta gente tiene cada juego y cuánto cuesta. No te dice cuánto dinero movió cada juego, ni qué porcentaje del catálogo es gratuito. Esas respuestas no están en la tabla. Tienes que fabricarlas. En el juego de crafteo, este es el momento en que dejas de clasificar botín y empiezas a craftear: objetos nuevos, construidos con los que ya tienes.
Qué aprenderás
- Qué es una variable sintética, y por qué no son "datos inventados"
- Cómo guardar tu limpieza como una sola tabla,
games_clean, con una columna que la base de datos calcula sola - La diferencia entre una columna calculada (un valor por fila) y una métrica (un valor por grupo)
- Cómo añadir ambas a un dataset en Superset
- Cómo una función de ventana escribe el valor de un grupo junto a cada fila, y te da la proporción sobre el total en una sola consulta
- La trampa: por qué
est_revenueno es lo que un juego ganó realmente
Columnas que nadie recogió¶
Una variable sintética es una columna que nadie recogió. Se calcula a partir de las columnas que sí se recogieron.
"Sintética" no significa falsa. Significa montada a partir de piezas. El total de línea de un tique no es mentira: es 3 × 1.20, y cualquiera puede comprobarlo. Una variable sintética es honesta por la misma razón: viene con su receta.
Ya has hecho algunas sin llamarlas así. En Categorización construiste price_band a partir de price. En Imputación construiste is_free y has_metascore. Hoy ponemos nombre a la idea y aprendemos las tres maneras de hacerlo. Usaremos el tique durante toda la página:
| En el tique | Qué es | En SQL y Superset |
|---|---|---|
| Total de línea (unidades × precio) | Un valor nuevo para cada fila, calculado solo con esa fila | Columna calculada: owners_min * price |
TOTAL de abajo |
Un valor para todo el grupo; las líneas desaparecen | Métrica: SUM(owners_min * price) |
| "Esta línea es el 40% de tu tique" | Un valor de grupo escrito junto a cada fila; las líneas se quedan | Función de ventana: SUM(...) OVER () |
flowchart LR
R["<b>Filas de la tabla</b><br>un juego por fila"]
R --> C["<b>Columna calculada</b><br>el total de línea:<br>entran 26,685 filas, salen 26,685 filas"]
R --> M["<b>Métrica</b><br>el TOTAL:<br>entran 26,685 filas, sale 1 fila por grupo"]
R --> W["<b>Función de ventana</b><br>la proporción del tique:<br>entran 26,685 filas, salen 26,685 filas,<br>cada una con el valor de su grupo"]
Primero, haz permanente la limpieza¶
Cada arreglo que escribiste en esta unidad era un SELECT. Mostraba un resultado limpio y desaparecía al cerrar la pestaña. La tabla games nunca cambió. Antes de craftear nada nuevo, guardas la limpieza como una tabla de verdad.
Ya conoces la herramienta de Datasets y destino: CREATE TABLE ... AS SELECT guarda el resultado de una consulta como una tabla nueva. El guion de clase games_clean.sql reúne todos los pasos de limpieza de la unidad en una sola sentencia. Léelo y reconocerás cada línea:
| Línea del script | Dónde la aprendiste |
|---|---|
TRIM(game), y release_date de texto a un DATE de verdad con TO_DATE |
Tipos y formatos |
owners → owners_min: SPLIT_PART + REGEXP_REPLACE + ::bigint |
Unidades y duplicados |
price_band y owners_tier con CASE WHEN |
Categorización |
COALESCE(price, 0) + is_free, 'Unknown', has_metascore, WHERE game IS NOT NULL |
Imputación |
ALTER TABLE ... ADD COLUMN release_year ... GENERATED ALWAYS AS (...) STORED |
Hoy |
El script completo, games_clean.sql
DROP TABLE IF EXISTS games_clean;
CREATE TABLE games_clean AS
SELECT
number,
TRIM(game) AS game,
COALESCE(NULLIF(TRIM(developer), ''), 'Unknown') AS developer,
COALESCE(NULLIF(TRIM(publisher), ''), 'Unknown') AS publisher,
CASE WHEN release_date ~ '^[A-Z][a-z]{2} [0-9]{1,2}, [0-9]{4}$' THEN TO_DATE(release_date, 'Mon DD, YYYY')
WHEN release_date ~ '^[A-Z][a-z]{2} [0-9]{4}$' THEN TO_DATE(release_date, 'Mon YYYY')
END AS release_date,
COALESCE(price, 0)::numeric(8,2) AS price,
(price IS NULL) AS is_free,
CASE WHEN price IS NULL THEN 'Free' WHEN price < 5 THEN 'Under 5' WHEN price < 15 THEN '5 to 15'
WHEN price < 30 THEN '15 to 30' ELSE '30 or more' END AS price_band,
REGEXP_REPLACE(SPLIT_PART(owners, '..', 1), '[^0-9]', '', 'g')::bigint AS owners_min,
CASE WHEN REGEXP_REPLACE(SPLIT_PART(owners, '..', 1), '[^0-9]', '', 'g')::bigint < 20000 THEN 'Niche'
WHEN REGEXP_REPLACE(SPLIT_PART(owners, '..', 1), '[^0-9]', '', 'g')::bigint < 1000000 THEN 'Mid'
ELSE 'Hit' END AS owners_tier,
average_playtime, median_playtime,
metascore,
(metascore IS NOT NULL) AS has_metascore
FROM games WHERE game IS NOT NULL;
ALTER TABLE games_clean ADD COLUMN release_year int
GENERATED ALWAYS AS (EXTRACT(YEAR FROM release_date)::int) STORED;
Empieza con DROP TABLE IF EXISTS, así que puedes ejecutarlo tantas veces como quieras: siempre reconstruye la tabla desde cero.
La última línea es la primera variable sintética del día.
Una columna generada es una columna que la base de datos calcula por sí sola a partir de otras columnas de la misma fila, y mantiene al día.
Nadie escribe un release_year. PostgreSQL lee release_date y escribe el año a su lado:
SELECT game, release_date, release_year
FROM games_clean ORDER BY number LIMIT 3;
| game | release_date | release_year |
|---|---|---|
| Half-Life 2 | 2004-11-16 | 2004 |
| Sid Meier's Civilization IV | 2005-10-25 | 2005 |
| Company of Heroes | 2006-09-11 | 2006 |
Comprueba que tu tabla está completa:
SELECT COUNT(*) FROM games_clean;
La respuesta es 26,685: las 26,688 filas de games menos los tres juegos sin nombre.
Después registra la tabla para que Superset pueda graficarla: Datasets → + Dataset → Database Dades classe, Schema public, Table games_clean → Create dataset (la flecha junto a Create and explore dataset).
Guarda esta tabla
games_clean es la tabla de la que se dibuja cada gráfico de la Unidad 3 y cada cuadro de mando (dashboard) de la Unidad 4. Constrúyela una vez, comprueba los 26,685 y déjala en paz.
El total de línea: una columna calculada¶
Una columna calculada es una columna nueva cuyo valor se calcula fila por fila, usando solo los valores de esa misma fila.
En el tique es "3 × 1.20 = 3.60". La caja no necesita nada más que esa línea para imprimirlo.
Nuestro total de línea: ¿cuánto dinero movió un juego, más o menos? Toma los propietarios y multiplícalos por el precio. Lo llamamos est_revenue, de estimated revenue (ingresos estimados).
SELECT game, owners_min, price, owners_min * price AS est_revenue
FROM games_clean ORDER BY est_revenue DESC LIMIT 5;
| game | owners_min | price | est_revenue |
|---|---|---|---|
| PLAYERUNKNOWN'S BATTLEGROUNDS | 50,000,000 | 29.99 | 1,499,500,000.00 |
| MONSTER HUNTER: WORLD | 5,000,000 | 59.99 | 299,950,000.00 |
| Sid Meier's Civilization V | 10,000,000 | 29.99 | 299,900,000.00 |
| Grand Theft Auto V | 10,000,000 | 29.99 | 299,900,000.00 |
| ARK: Survival Evolved | 5,000,000 | 49.99 | 249,950,000.00 |
Lee la primera cifra en voz alta: mil cuatrocientos noventa y nueve millones. Unos 1,500 millones de dólares. Un número tan grande merece una segunda mirada, y se la daremos al final de esta página.
En SQL Lab esa columna vive solo dentro de tu consulta. Para tenerla lista para todos los gráficos futuros, añádela al dataset:
| Paso | Haz esto | Deberías ver |
|---|---|---|
| 1 | Datasets → en la fila games_clean, haz clic en el lápiz bajo Actions (Edit) |
El editor del dataset |
| 2 | Pestaña Calculated columns → Add item | Una fila nueva vacía |
| 3 | Pon a la columna el nombre est_revenue |
|
| 4 | En SQL expression, borra el texto de ejemplo y escribe owners_min * price |
|
| 5 | Data type: NUMERIC |
|
| 6 | Save → OK | est_revenue aparece entre las columnas del dataset |
Otras columnas calculadas siguen el mismo patrón. El precio con un 21% de IVA es price * 1.21: entra una fila, sale un valor.
El TOTAL: una métrica¶
Una métrica es un cálculo sobre muchas filas que devuelve un valor por grupo. Siempre es un agregado:
SUM,AVG,COUNT,MIN,MAX.
En el tique es el TOTAL. Las líneas desaparecen y queda un solo número. Conociste los agregados en SQL Lab: grupos; una métrica es un agregado con nombre, guardado en el dataset para que nunca tengas que reescribirlo.
Añade dos, en el mismo editor: pestaña Metrics → Add item, dos veces.
| Clave de la métrica | Expresión SQL | Qué responde |
|---|---|---|
total_est_revenue |
SUM(owners_min * price) |
¿Cuántos ingresos estimados hay en total? |
pct_free |
100.0 * SUM(CASE WHEN is_free THEN 1 ELSE 0 END) / COUNT(*) |
¿Qué porcentaje de los juegos es gratuito? |
Luego Save → OK.
Mira pct_free. CASE WHEN is_free THEN 1 ELSE 0 END convierte cada juego en un 1 o un 0. Sumarlos cuenta los gratuitos. Dividir entre COUNT(*) lo convierte en una proporción. El 100.0, con su punto decimal, evita que PostgreSQL haga una división entera y responda 0.
Una métrica no tiene un tamaño fijo. Responde para el grupo que le pases. Para toda la tabla:
SELECT SUM(owners_min * price) AS total_est_revenue,
ROUND(100.0 * SUM(CASE WHEN is_free THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_free
FROM games_clean;
| total_est_revenue | pct_free |
|---|---|
| 26,685,821,600.00 | 11.6 |
Y con un GROUP BY owners_tier, las mismas dos métricas responden tres veces:
SELECT owners_tier,
SUM(owners_min * price) AS total_est_revenue,
ROUND(100.0 * SUM(CASE WHEN is_free THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_free
FROM games_clean GROUP BY owners_tier ORDER BY owners_tier;
| owners_tier | total_est_revenue | pct_free |
|---|---|---|
| Hit | 18,256,160,000.00 | 29.2 |
| Mid | 8,429,661,600.00 | 18.5 |
| Niche | 0.00 | 7.8 |
Casi tres de cada diez Hits son gratuitos. ¿Y el nivel Niche ganó... cero? Guarda ese pensamiento.
Ojo: la métrica no ve tu columna calculada
Es tentador escribir la métrica como SUM(est_revenue). Superset la guarda sin quejarse. Luego el gráfico falla: column "est_revenue" does not exist.
La columna calculada vive en el dataset de Superset, no en la tabla de PostgreSQL. La caja imprime el total de línea; la etiqueta de la estantería nunca lo tuvo. Escribe la métrica con la expresión completa: SUM(owners_min * price).
El error opuesto es igual de malo: nunca pongas un agregado dentro de una columna calculada. Una línea del tique no puede contener el TOTAL.
La proporción del tique: funciones de ventana¶
A veces quieres las dos cosas a la vez: todas las filas, y el total al lado. "Esta línea es el 40% de tu tique." Con GROUP BY no se puede: las filas han desaparecido.
Una función de ventana calcula un valor sobre un grupo de filas y lo escribe junto a cada fila, sin colapsarlas. La reconoces por la palabra
OVER.
Lee OVER () como "sobre todas las filas, sin colapsarlas". Aquí da la proporción de cada franja de precio:
SELECT price_band, COUNT(*) AS games,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_of_games
FROM games_clean GROUP BY price_band ORDER BY games DESC;
| price_band | games | pct_of_games |
|---|---|---|
| Under 5 | 11,613 | 43.5 |
| 5 to 15 | 9,046 | 33.9 |
| Free | 3,094 | 11.6 |
| 15 to 30 | 2,367 | 8.9 |
| 30 or more | 565 | 2.1 |
COUNT(*) es la línea. SUM(COUNT(*)) OVER () es el TOTAL, impreso en cada línea: 26,685 cinco veces. Divide uno entre otro y tienes una proporción sobre el total en una consulta, sin subconsulta.
Una ventana también se puede cortar en grupos.
PARTITION BYes elGROUP BYde una ventana: divide las filas en grupos, pero las filas se quedan.
Esta consulta compara cada juego con el precio medio de su propio año de lanzamiento:
SELECT game, release_year, price,
ROUND(AVG(price) OVER (PARTITION BY release_year), 2) AS year_avg,
ROUND(price - AVG(price) OVER (PARTITION BY release_year), 2) AS vs_year_avg
FROM games_clean ORDER BY owners_min DESC, number LIMIT 5;
| game | release_year | price | year_avg | vs_year_avg |
|---|---|---|---|---|
| Dota 2 | 2013 | 0.00 | 11.21 | -11.21 |
| Team Fortress 2 | 2007 | 0.00 | 8.59 | -8.59 |
| PLAYERUNKNOWN'S BATTLEGROUNDS | 2017 | 29.99 | 7.69 | 22.30 |
| Counter-Strike: Global Offensive | 2012 | 0.00 | 10.96 | -10.96 |
| Paladins | 2018 | 0.00 | 7.12 | -7.12 |
Cinco filas, cinco juegos, y cada uno lleva la media de su año. PUBG costó 22.30 dólares más que el juego medio de 2017. vs_year_avg es una variable sintética construida a partir de un cálculo agregado.
GROUP BY |
Función de ventana | |
|---|---|---|
| Filas que recibes | Una por grupo | Todas |
| Dónde va el valor del grupo | Sustituye a las filas | Se sienta junto a cada fila |
| Palabra clave | GROUP BY publisher |
OVER (PARTITION BY publisher) |
| En el tique | El TOTAL |
"Esta línea es el 40% de tu tique" |
Ojo: WHERE se ejecuta antes que la ventana
Si filtras con WHERE, el "total" es el total de lo que queda. Añade WHERE price_band <> 'Free' a una consulta de proporciones y los porcentajes siguen sumando 100, pero es el 100% de los juegos de pago, no del catálogo. Di siempre en voz alta qué es tu 100%.
Lo que est_revenue no es¶
Ahora la segunda mirada que prometimos. Aquí tienes la proporción de juegos y la proporción de ingresos estimados por nivel:
SELECT owners_tier, COUNT(*) AS games,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_games,
ROUND(100.0 * SUM(owners_min * price) / SUM(SUM(owners_min * price)) OVER (), 1) AS pct_revenue
FROM games_clean GROUP BY owners_tier ORDER BY pct_revenue DESC;
| owners_tier | games | pct_games | pct_revenue |
|---|---|---|---|
| Hit | 623 | 2.3 | 68.4 |
| Mid | 8,141 | 30.5 | 31.6 |
| Niche | 17,921 | 67.2 | 0.0 |
623 juegos de 26,685 se llevan dos tercios del dinero. Es un patrón real e interesante.
¿Pero 17,921 juegos ganaron nada? No. Recuerda de dónde salió owners_min: del extremo inferior de un rango. El archivo original dice "0 .. 20,000", y el extremo inferior de ese rango es 0. Cero propietarios por cualquier precio es cero.
La variable es tan buena como las columnas con las que se hizo. Así que est_revenue debe viajar con su etiqueta:
| Qué es | Qué no es |
|---|---|
| El número mínimo de propietarios × el precio el día en que se recogieron los datos | Lo que el juego ganó de verdad |
| Un suelo, útil para comparar juegos entre sí | Una cantidad que puedas citar en euros o dólares |
| 0 para cada juego gratuito, Dota 2 incluido | Prueba de que los juegos gratuitos no ganan dinero |
| Ciega a ventas, descuentos y cambios de precio | Un registro de ventas |
Una variable sintética sin su receta es un rumor
Siempre que crees una columna, deja escrito cómo. est_revenue = owners_min * price, donde owners_min es el extremo inferior de una estimación. Con esa línea, un lector puede juzgar tu número. Sin ella, solo puede creerlo o no.
Pruébalo¶
Usa games_clean en SQL Lab (base de datos Dades classe, esquema public). El archivo original, video_games.csv, está en la página de Recursos.
Paso 1 — Añade la columna calculada est_revenue y las métricas total_est_revenue y pct_free a tu dataset, siguiendo las tablas de arriba.
Paso 2 — Predice primero, ejecuta después: ¿qué nivel tiene el pct_free más alto? Compruébalo con la consulta GROUP BY owners_tier.
Paso 3 — Completa la frase con la última consulta: "El nivel Hit es el __% de los juegos y el __% de los ingresos estimados."
Paso 4 — Clasifica dentro de un grupo. RANK() es una función de ventana que numera las filas de cada grupo. ¿Cuáles son los juegos con más propietarios de 2017?
SELECT game, owners_min,
RANK() OVER (PARTITION BY release_year ORDER BY owners_min DESC) AS position
FROM games_clean WHERE release_year = 2017 ORDER BY position LIMIT 4;
Deberías obtener PLAYERUNKNOWN'S BATTLEGROUNDS (1), Unturned (2), y Robocraft y Brawlhalla compartiendo la posición 3: empatan a 10,000,000.
Paso 5 — Escribe una razón por la que est_revenue no es lo que un juego ganó realmente. Si puedes, has entendido la lección.
Ideas clave¶
- Una variable sintética es una columna que nadie recogió: se calcula a partir de las que sí se recogieron. Es honesta mientras lleve su receta.
CREATE TABLE ... AS SELECThace permanente la limpieza engames_clean(26,685 filas). Una columna generada comorelease_yearla calcula la propia base de datos.- Una columna calculada da un valor por fila (
owners_min * price). Una métrica da un valor por grupo (SUM(owners_min * price)). - Una métrica debe usar la expresión completa: las columnas calculadas de Superset no existen en la tabla de PostgreSQL.
- Una función de ventana (
OVER,PARTITION BY) escribe un valor de grupo junto a cada fila.COUNT(*) / SUM(COUNT(*)) OVER ()es una proporción sobre el total. est_revenuees un suelo, no unos ingresos: una variable es tan buena como las columnas con las que se hizo.
Comprobación rápida¶
1. ¿Qué es una variable sintética?
- Una columna llena de valores inventados para esconder datos ausentes
- Una columna que nadie recogió, calculada a partir de las columnas que sí se recogieron
- Una columna copiada de otra tabla con un
JOIN - Una columna cuyos valores se escribieron a mano tras la subida
Sintética significa montada a partir de piezas, no falsa. release_year, price_band y est_revenue nunca estuvieron en el archivo: cada una se calcula a partir de columnas existentes con una receta que cualquiera puede comprobar.
2. En el tique del supermercado, ¿qué parte es la métrica?
- El precio impreso en la etiqueta de la estantería
- El total de línea, unidades × precio
- El TOTAL de abajo
- La nota "esta línea es el 40% de tu tique"
Una métrica es un agregado: entran muchas filas y sale un valor por grupo, como el TOTAL. El total de línea es una columna calculada y la nota del 40% es una función de ventana.
3. Creaste la columna calculada est_revenue en el editor del dataset y luego una métrica SUM(est_revenue). El gráfico falla y dice que la columna est_revenue no existe. ¿Por qué?
- Porque
SUMno funciona con columnasNUMERIC - Porque un dataset puede tener una columna calculada o una métrica, pero no ambas
- Porque la columna calculada vive en el dataset de Superset, no en la tabla de PostgreSQL
- Porque los nombres de métricas no pueden contener guion bajo
La consulta se envía a PostgreSQL, y la tabla games_clean no tiene ninguna columna llamada est_revenue: esa columna existe solo en el dataset de Superset. Escribe la métrica con la expresión completa, SUM(owners_min * price).
4. ¿Cuál es la diferencia entre GROUP BY publisher y OVER (PARTITION BY publisher)?
- Ninguna: son dos grafías de lo mismo
-
GROUP BYfunciona con números yPARTITION BYcon texto -
PARTITION BYborra las filas que no pertenecen a un grupo -
GROUP BYdevuelve una fila por editora; la ventana devuelve todas las filas con el valor de su editora al lado
Ambos dividen las filas en grupos. GROUP BY colapsa cada grupo en una fila. Una función de ventana conserva todas las filas y escribe el valor del grupo junto a ellas, por eso puedes restar el precio de un juego a la media de su año.
5. En la consulta de proporciones, ¿qué te da SUM(COUNT(*)) OVER ()?
- El número de juegos de cada franja de precio
- El número total de juegos, repetido en cada fila del resultado
- El número de franjas de precio
- Un total acumulado que crece fila a fila
COUNT(*) es la línea: los juegos de una franja. SUM(COUNT(*)) OVER () suma esos recuentos sobre todas las filas e imprime el resultado, 26,685, en cada una. Dividir el primero entre el segundo da la proporción sobre el total.
6. est_revenue dice que los 17,921 juegos Niche ganaron 0 en total. ¿Cuál es la conclusión correcta?
- Todos los juegos Niche son gratuitos
- La consulta tiene un error en el
SUM -
owners_mines el extremo inferior del rango0 .. 20,000, así que la variable es un suelo, no unos ingresos reales - Steam no paga a los desarrolladores pequeños
La aritmética es correcta: 0 propietarios × cualquier precio = 0. El problema es el ingrediente. owners_min es el extremo inferior de una estimación, y para el rango más pequeño ese extremo es 0. Una variable sintética es tan buena como las columnas con las que se hizo.
7. Para un dataset nuevo necesitas: el precio de cada juego con un 21% de IVA, y el tiempo de juego medio por editora. Elige las dos opciones correctas.
- El precio con IVA es una columna calculada:
price * 1.21 - El tiempo de juego medio por editora es una métrica:
AVG(average_playtime), agrupada por editora - El precio con IVA es una métrica, porque usa una multiplicación
- El tiempo de juego medio por editora es una columna calculada, porque cada juego tiene una editora
Pregúntate "¿cuántos valores obtengo?". El precio con IVA es un valor por fila, solo a partir de esa fila: una columna calculada. La media es un valor por grupo: una métrica. Si quisieras la posición de cada juego entre los juegos de su editora, necesitarías una función de ventana: un cálculo de grupo que conserva todas las filas.
games_clean ya existe, con columnas que nunca estuvieron en el archivo. A continuación reconstruyes la misma limpieza sin escribir una línea de SQL: Power Query, donde haces clic y la herramienta escribe la receta por ti.