Salta el contingut

Variables sintètiques

Exercici synthetic-variables-annex.pdf · PDF

Compres tres coses al supermercat i mires el tiquet. El preu de cada producte era al prestatge. Però la línia que diu "3 × 1.20 = 3.60", i el TOTAL de baix de tot? Ningú no les va enganxar a cap prestatge. Les va calcular la caixa.

Les teves dades són igual. El fitxer dels jocs de Steam et diu quanta gent té cada joc i què costa. No et diu quants diners ha mogut cada joc, ni quina part del catàleg és gratuïta. Aquestes respostes no són a la taula. Les has de fabricar. Al joc de crafting, aquest és el moment en què deixes de classificar botí i comences a fabricar: objectes nous, construïts amb els que ja tens.

Què aprendràs

  • Què és una variable sintètica, i per què no són "dades inventades"
  • Com desar la teva neteja com una sola taula, games_clean, amb una columna que la base de dades calcula tota sola
  • La diferència entre una columna calculada (un valor per fila) i una mètrica (un valor per grup)
  • Com afegir-les totes dues a un dataset de Superset
  • Com una funció de finestra escriu el valor d'un grup al costat de cada fila, i et dona una proporció del total en una sola consulta
  • La trampa: per què est_revenue no és el que un joc va guanyar realment

Columnes que ningú no ha recollit

Una variable sintètica és una columna que ningú no ha recollit. Es calcula a partir de les columnes que sí que es van recollir.

"Sintètica" no vol dir falsa. Vol dir composta. El total d'una línia del tiquet no és una mentida: és 3 × 1.20, i qualsevol ho pot comprovar. Una variable sintètica és honesta pel mateix motiu: ve amb una recepta.

Ja n'has fet algunes sense dir-ne així. A Categorització vas construir price_band a partir de price. A Imputació vas construir is_free i has_metascore. Avui posem nom a la idea i aprenem les tres maneres de fer-ho. Mantindrem el tiquet durant tota la pàgina:

Al tiquet Què és A SQL i Superset
Total de línia (unitats × preu) Un valor nou per a cada fila, fet només amb aquella fila Columna calculada: owners_min * price
TOTAL de baix de tot Un valor per a tot el grup; les línies desapareixen Mètrica: SUM(owners_min * price)
"Aquesta línia és el 40% del teu tiquet" Un valor de grup escrit al costat de cada fila; les línies es queden Funció de finestra: SUM(...) OVER ()
flowchart LR
    R["<b>Files de la taula</b><br>un joc per fila"]
    R --> C["<b>Columna calculada</b><br>el total de línia:<br>26,685 files d'entrada, 26,685 files de sortida"]
    R --> M["<b>Mètrica</b><br>el TOTAL:<br>26,685 files d'entrada, 1 fila per grup de sortida"]
    R --> W["<b>Funció de finestra</b><br>la proporció del tiquet:<br>26,685 files d'entrada, 26,685 files de sortida,<br>cadascuna amb el valor del seu grup"]

Primer, fes permanent la neteja

Cada arranjament que has escrit en aquesta unitat era un SELECT. Mostrava un resultat net i desapareixia quan tancaves la pestanya. La taula games no va canviar mai. Abans de fabricar res de nou, desa la neteja com una taula real.

Ja coneixes l'eina de Datasets i destí: CREATE TABLE ... AS SELECT desa el resultat d'una consulta com una taula nova. El guió de classe games_clean.sql posa tots els passos de neteja de la unitat en una sola sentència. Llegeix-lo i reconeixeràs cada línia:

Línia del guió On ho vas aprendre
TRIM(game), i release_date de text a un DATE de veritat amb TO_DATE Tipus i formats
owners → owners_min: SPLIT_PART + REGEXP_REPLACE + ::bigint Unitats i duplicats
price_band i owners_tier amb CASE WHEN Categorització
COALESCE(price, 0) + is_free, 'Unknown', has_metascore, WHERE game IS NOT NULL Imputació
ALTER TABLE ... ADD COLUMN release_year ... GENERATED ALWAYS AS (...) STORED Avui
El guió complet, 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;

Comença amb DROP TABLE IF EXISTS, així que el pots executar tantes vegades com vulguis: sempre reconstrueix la taula des de zero.

L'última línia és la primera variable sintètica del dia.

Una columna generada és una columna que la base de dades calcula tota sola a partir d'altres columnes de la mateixa fila, i manté actualitzada.

Ningú no escriu un release_year. PostgreSQL llegeix release_date i n'escriu l'any al costat:

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

Comprova que la teva taula és completa:

SELECT COUNT(*) FROM games_clean;

La resposta és 26,685: les 26,688 files de games menys els tres jocs sense nom.

Després registra la taula perquè Superset la pugui representar: Datasets → + Dataset → Database Dades classe, Schema public, Table games_clean → Create dataset (la fletxa al costat de Create and explore dataset).

Guarda aquesta taula

games_clean és la taula de la qual es dibuixen tots els gràfics de la Unitat 3 i tots els quadres de comandament (dashboards) de la Unitat 4. Construeix-la una vegada, comprova els 26,685, i deixa-la en pau.

El total de línia: una columna calculada

Una columna calculada és una columna nova el valor de la qual es calcula fila per fila, fent servir només els valors d'aquella mateixa fila.

Al tiquet és "3 × 1.20 = 3.60". La caixa no necessita res més que aquella línia per imprimir-ho.

El nostre total de línia: quants diners va moure un joc, més o menys? Pren els propietaris i multiplica pel preu. L'anomenem est_revenue, de estimated revenue (ingressos estimats).

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

Llegeix la primera xifra en veu alta: mil quatre-cents noranta-nou milions. Uns 1.500 milions de dòlars. Un número tan gros mereix una segona mirada, i li la donarem al final d'aquesta pàgina.

A SQL Lab aquesta columna només viu dins de la teva consulta. Per tenir-la a punt per a tots els gràfics futurs, afegeix-la al dataset:

Pas Fes això Hauries de veure
1 Datasets → a la fila games_clean, clica el llapis sota Actions (Edit) L'editor del dataset
2 Pestanya Calculated columns → Add item Una fila nova buida
3 Posa de nom a la columna est_revenue
4 A SQL expression, esborra el text d'exemple i escriu owners_min * price
5 Data type: NUMERIC
6 Save → OK est_revenue apareix entre les columnes del dataset

Les altres columnes calculades segueixen el mateix patró. El preu amb un 21% d'IVA és price * 1.21: una fila d'entrada, un valor de sortida.

El TOTAL: una mètrica

Una mètrica és un càlcul sobre moltes files que retorna un valor per grup. Sempre és un agregat: SUM, AVG, COUNT, MIN, MAX.

Al tiquet és el TOTAL. Les línies desapareixen i queda un sol número. Vas conèixer els agregats a SQL Lab: grups; una mètrica és un agregat amb nom, desat al dataset perquè no l'hagis de tornar a escriure mai.

N'afegeixes dues, al mateix editor: pestanya Metrics → Add item, dues vegades.

Clau de la mètrica Expressió SQL Què respon
total_est_revenue SUM(owners_min * price) Quants ingressos estimats hi ha en total?
pct_free 100.0 * SUM(CASE WHEN is_free THEN 1 ELSE 0 END) / COUNT(*) Quin percentatge dels jocs és gratuït?

Després Save → OK.

Mira pct_free. CASE WHEN is_free THEN 1 ELSE 0 END converteix cada joc en un 1 o un 0. Sumar-los compta els gratuïts. Dividir entre COUNT(*) en fa una proporció. El 100.0, amb el seu punt decimal, evita que PostgreSQL faci una divisió entera i respongui 0.

Una mètrica no té una mida fixa. Respon per al grup que li donis. Per a tota la taula:

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

I amb un GROUP BY owners_tier, les mateixes dues mètriques responen tres vegades:

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

Gairebé tres de cada deu Hits són gratuïts. I el nivell Niche ha guanyat... zero? Guarda't aquest pensament.

Compte: la mètrica no pot veure la teva columna calculada

Temptador: escriure la mètrica com SUM(est_revenue). Superset la desa sense queixar-se. Després el gràfic falla: column "est_revenue" does not exist.

La columna calculada viu al dataset de Superset, no a la taula de PostgreSQL. La caixa imprimeix el total de línia; l'etiqueta del prestatge mai no el va tenir. Escriu la mètrica amb l'expressió completa: SUM(owners_min * price).

L'error contrari és igual de dolent: mai no posis un agregat dins d'una columna calculada. Una línia del tiquet no pot contenir el TOTAL.

La proporció del tiquet: funcions de finestra

De vegades vols les dues coses alhora: cada fila, i el total al costat. "Aquesta línia és el 40% del teu tiquet." Amb GROUP BY no pots: les files ja no hi són.

Una funció de finestra calcula un valor sobre un grup de files i l'escriu al costat de cada fila, sense col·lapsar-les. La reconeixes per la paraula OVER.

Llegeix OVER () com "sobre totes les files, sense col·lapsar-les". Aquí dona la proporció de cada franja de preu:

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(*) és la línia. SUM(COUNT(*)) OVER () és el TOTAL, imprès a cada línia: 26,685 cinc vegades. Divideix l'un per l'altre i tens una proporció del total en una sola consulta, sense cap subconsulta.

Una finestra també es pot tallar en grups.

PARTITION BY és el GROUP BY d'una finestra: divideix les files en grups, però les files es queden.

Aquesta consulta compara cada joc amb el preu mitjà del seu propi any de llançament:

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

Cinc files, cinc jocs, i cadascun porta la mitjana del seu any. PUBG va costar 22.30 dòlars més que el joc mitjà del 2017. vs_year_avg és una variable sintètica construïda a partir d'un càlcul d'agregació.

GROUP BY Funció de finestra
Files que obtens Una per grup Totes les files
On va el valor del grup Substitueix les files Seu al costat de cada fila
Paraula clau GROUP BY publisher OVER (PARTITION BY publisher)
Al tiquet El TOTAL "Aquesta línia és el 40% del teu tiquet"

Compte: WHERE s'executa abans de la finestra

Si filtres amb WHERE, el "total" és el total del que queda. Afegeix WHERE price_band <> 'Free' a una consulta de proporcions i els percentatges continuen sumant 100, però és el 100% dels jocs de pagament, no del catàleg. Digues sempre en veu alta què és el teu 100%.

Què no és est_revenue

Ara, la segona mirada que havíem promès. Aquí tens la proporció de jocs i la proporció d'ingressos estimats per nivell:

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 jocs de 26,685 s'enduen dos terços dels diners. Aquest és un patró real i interessant.

Però 17,921 jocs no han guanyat res? No. Recorda d'on venia owners_min: de la part baixa d'un rang. El fitxer original diu "0 .. 20,000", i la part baixa d'aquest rang és 0. Zero propietaris per qualsevol preu és zero.

La variable només és tan bona com les columnes amb què s'ha fet. Així que est_revenue ha de viatjar amb la seva etiqueta:

Què és Què no és
El mínim de propietaris × el preu el dia que es van recollir les dades El que el joc va guanyar realment
Un mínim, útil per comparar jocs entre ells Una quantitat que puguis citar en euros o dòlars
0 per a cada joc gratuït, Dota 2 inclòs Una prova que els jocs gratuïts no guanyen diners
Cega a les vendes, els descomptes i els canvis de preu Un registre de vendes

Una variable sintètica sense la seva recepta és un rumor

Cada vegada que creïs una columna, deixa escrit com. est_revenue = owners_min * price, on owners_min és l'extrem baix d'una estimació. Amb aquesta línia, un lector pot jutjar el teu número. Sense ella, només pot creure-se'l o no.

Prova-ho

Fes servir games_clean a SQL Lab (base de dades Dades classe, esquema public). El fitxer original, video_games.csv, és a la pàgina de Recursos.

Pas 1 — Afegeix la columna calculada est_revenue i les mètriques total_est_revenue i pct_free al teu dataset, seguint les taules de dalt.

Pas 2 — Primer prediu, després executa: quin nivell té el pct_free més alt? Comprova-ho amb la consulta GROUP BY owners_tier.

Pas 3 — Acaba la frase amb l'última consulta: "El nivell Hit és el __% dels jocs i el __% dels ingressos estimats."

Pas 4 — Fes un rànquing dins d'un grup. RANK() és una funció de finestra que numera les files de cada grup. Quins són els jocs amb més propietaris del 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;

Hauries d'obtenir PLAYERUNKNOWN'S BATTLEGROUNDS (1), Unturned (2), i Robocraft i Brawlhalla compartint la posició 3: empaten a 10,000,000.

Pas 5 — Escriu una raó per la qual est_revenue no és el que un joc va guanyar realment. Si en saps escriure una, has entès la lliçó.

Idees clau

  • Una variable sintètica és una columna que ningú no ha recollit: es calcula a partir de les que sí. És honesta mentre porti la seva recepta.
  • CREATE TABLE ... AS SELECT fa permanent la neteja a games_clean (26,685 files). Una columna generada com release_year la calcula la mateixa base de dades.
  • Una columna calculada dona un valor per fila (owners_min * price). Una mètrica dona un valor per grup (SUM(owners_min * price)).
  • Una mètrica ha d'usar l'expressió completa: les columnes calculades de Superset no existeixen a la taula de PostgreSQL.
  • Una funció de finestra (OVER, PARTITION BY) escriu el valor d'un grup al costat de cada fila. COUNT(*) / SUM(COUNT(*)) OVER () és una proporció del total.
  • est_revenue és un mínim, no uns ingressos: una variable només és tan bona com les columnes amb què s'ha fet.

Comprovació ràpida

1. Què és una variable sintètica?
  • Una columna omplerta amb valors inventats per amagar dades absents
  • Una columna que ningú no ha recollit, calculada a partir de les columnes que sí
  • Una columna copiada d'una altra taula amb un JOIN
  • Una columna els valors de la qual s'han escrit a mà després de la càrrega

Sintètica vol dir composta, no falsa. release_year, price_band i est_revenue mai no van ser al fitxer: cadascuna es calcula a partir de columnes existents amb una recepta que qualsevol pot comprovar.

2. Al tiquet del supermercat, quina part és la mètrica?
  • El preu imprès a l'etiqueta del prestatge
  • El total de línia, unitats × preu
  • El TOTAL de baix de tot
  • La nota "aquesta línia és el 40% del teu tiquet"

Una mètrica és un agregat: entren moltes files i surt un valor per grup, com el TOTAL. El total de línia és una columna calculada i la nota del 40% és una funció de finestra.

3. Has creat la columna calculada est_revenue a l'editor del dataset i després una mètrica SUM(est_revenue). El gràfic falla i diu que la columna est_revenue no existeix. Per què?
  • Perquè SUM no funciona amb columnes NUMERIC
  • Perquè un dataset pot tenir una columna calculada o una mètrica, però no totes dues
  • Perquè la columna calculada viu al dataset de Superset, no a la taula de PostgreSQL
  • Perquè els noms de les mètriques no poden contenir un guió baix

La consulta s'envia a PostgreSQL, i la taula games_clean no té cap columna anomenada est_revenue: aquesta columna només existeix al dataset de Superset. Escriu la mètrica amb l'expressió completa, SUM(owners_min * price).

4. Quina diferència hi ha entre GROUP BY publisher i OVER (PARTITION BY publisher)?
  • Cap: són dues grafies de la mateixa cosa
  • GROUP BY funciona amb números i PARTITION BY funciona amb text
  • PARTITION BY esborra les files que no pertanyen a un grup
  • GROUP BY retorna una fila per editor; la finestra retorna cada fila amb el valor del seu editor al costat

Les dues divideixen les files en grups. GROUP BY col·lapsa cada grup en una fila. Una funció de finestra conserva cada fila i hi escriu al costat el valor del grup, per això pots restar el preu d'un joc de la mitjana del seu any.

5. A la consulta de proporcions, què et dona SUM(COUNT(*)) OVER ()?
  • El nombre de jocs de cada franja de preu
  • El nombre total de jocs, repetit a cada fila del resultat
  • El nombre de franges de preu
  • Un total acumulat que creix fila a fila

COUNT(*) és la línia: els jocs d'una franja. SUM(COUNT(*)) OVER () suma aquests recomptes sobre totes les files i imprimeix el resultat, 26,685, a cadascuna. Dividir el primer pel segon dona la proporció del total.

6. est_revenue diu que els 17,921 jocs Niche han guanyat 0 en total. Quina és la conclusió correcta?
  • Els jocs Niche són tots gratuïts
  • La consulta té un error al SUM
  • owners_min és la part baixa del rang 0 .. 20,000, així que la variable és un mínim, no uns ingressos reals
  • Steam no paga els desenvolupadors petits

L'aritmètica és correcta: 0 propietaris × qualsevol preu = 0. El problema és l'ingredient. owners_min és l'extrem baix d'una estimació, i per al rang més petit aquest extrem és 0. Una variable sintètica només és tan bona com les columnes amb què s'ha fet.

7. Per a un dataset nou necessites: el preu de cada joc amb un 21% d'IVA, i el temps de joc mitjà per editor. Tria les dues opcions correctes.
  • El preu amb IVA és una columna calculada: price * 1.21
  • El temps de joc mitjà per editor és una mètrica: AVG(average_playtime), agrupada per editor
  • El preu amb IVA és una mètrica, perquè fa servir una multiplicació
  • El temps de joc mitjà per editor és una columna calculada, perquè cada joc té un editor

Pregunta't "quants valors obtinc?". El preu amb IVA és un valor per fila, només d'aquella fila: una columna calculada. La mitjana és un valor per grup: una mètrica. Si volguessis la posició de cada joc entre els jocs del seu editor, necessitaries una funció de finestra: un càlcul de grup que conserva cada fila.

games_clean ja existeix, amb columnes que mai no van ser al fitxer. A continuació reconstruiràs la mateixa neteja sense escriure ni una línia d'SQL: Power Query, on cliques i l'eina escriu la recepta per tu.