Saltar a contenido

Categorización

Ejercicio categorisation-annex.pdf · PDF

Abres un cofre en un juego y cae una espada. ¿Lees su probabilidad de aparición, "0.37%"? No. Miras el color de su nombre. Común, Rara, Épica, Legendaria. Una palabra y ya sabes si te la quedas.

Alguien decidió dónde acaba "Rara" y dónde empieza "Épica". Esa persona convirtió miles de números posibles en cuatro etiquetas, para que tú pudieras decidir en medio segundo. En esta página haces el mismo trabajo con games: 199 precios distintos se convierten en cinco franjas, y cada juego recibe un nivel según el tamaño de su audiencia.

Qué aprenderás

  • Explicar por qué convertimos un número en una categoría, y qué se pierde al hacerlo
  • Escribir un CASE WHEN con varias ramas y predecir qué rama toma una fila concreta
  • Explicar por qué importa el orden de las ramas, y dónde acaba un NULL
  • Construir price_band y owners_tier, y ordenarlas en su orden lógico en lugar del alfabético
  • Detectar una misma categoría escrita de dos formas y unificarla
  • Evitar la trampa: las franjas no están "en los datos", las has dibujado tú

De números a niveles de rareza

Cuenta cuántos precios distintos tiene la tabla:

SELECT COUNT(DISTINCT price) FROM games;   -- 199

199 precios distintos. Dibuja una barra por precio y tendrás 199 barras: ilegible. Nadie lee "probabilidad 0.37%". Todo el mundo lee "Épico".

Una categoría es una etiqueta que mete un valor en un grupo. Categorizar es sustituir muchos valores distintos por unas pocas etiquetas.

Un intervalo (o franja, en inglés bin o band) es uno de esos grupos cuando el valor original es un número: un rango con un límite inferior y uno superior.

La analogía de toda la página es el nivel de rareza: una categoría es un número que se ha vuelto legible.

El número La categoría
En el juego Probabilidad 0.37% Épico
En games Precio 9.99 5 to 15
Sirve para Calcular: suma, media, ordenar Leer, agrupar, comparar grupos
Cuántos valores 199 precios 5 franjas

Ojo: conserva también el número

Categorizar no sustituye el precio. Añade una etiqueta a su lado, en una columna nueva. Siempre puedes cortar un número en franjas nuevas más adelante. Pero nunca podrás recuperar el número a partir de una etiqueta.

CASE WHEN: la regla de clasificación

CASE WHEN es una columna calculada con condiciones: para cada fila da la etiqueta de la primera condición que se cumple.

El esqueleto:

CASE WHEN condition_1 THEN 'label 1'
     WHEN condition_2 THEN 'label 2'
     ELSE 'label for the rest' END AS new_column

Cada línea WHEN ... THEN ... es una rama. Tres reglas lo deciden todo:

Regla Qué significa
De arriba abajo Cada fila prueba las ramas en orden y se detiene en la primera que se cumple. El orden forma parte de la lógica
ELSE se queda con el resto Todo lo que no ha coincidido con nada, incluido lo que no habías previsto. Sin ELSE, el resto se convierte en NULL
NULL nunca es verdadero NULL < 5 no es verdadero ni falso: es desconocido. Un NULL se cuela por todas las comparaciones, hasta llegar a ELSE
flowchart TB
    R["<b>Una fila</b><br>price = ?"] --> W1{"price IS NULL?"}
    W1 -- "verdadero" --> L1["'Free'"]
    W1 -- "no verdadero" --> W2{"price under 5?"}
    W2 -- "verdadero" --> L2["'Under 5'"]
    W2 -- "no verdadero" --> W3{"price under 15?"}
    W3 -- "verdadero" --> L3["'5 to 15'"]
    W3 -- "no verdadero" --> W4{"price under 30?"}
    W4 -- "verdadero" --> L4["'15 to 30'"]
    W4 -- "no verdadero" --> E["<b>ELSE</b><br>'30 or more'"]

El juego gratuito que parecía caro

Prueba la regla más simple posible con tres juegos que conoces:

SELECT game, price,
       CASE WHEN price < 5 THEN 'Under 5' ELSE '5 or more' END AS band
FROM games WHERE game IN ('Half-Life 2', 'Dota 2', 'Portal') ORDER BY game;
game price band
Dota 2 5 or more
Half-Life 2 9.99 5 or more
Portal 9.99 5 or more

Dota 2 es gratis. Recibió la franja 5 or more. Su precio es NULL, NULL < 5 no es verdadero, así que la fila cayó en ELSE. El juego con más propietarios de toda la tabla, y gratuito, etiquetado como caro. Sin ningún mensaje de error.

Ojo: ELSE no significa los caros

ELSE significa todo lo demás, y "todo lo demás" incluye las celdas vacías. Si un NULL merece su propia etiqueta, nómbralo el primero, a propósito, con WHEN column IS NULL.

Construir price_band

Cinco franjas, precios en dólares estadounidenses. El caso NULL va el primero:

SELECT 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,
       COUNT(*) AS n
FROM games WHERE game IS NOT NULL
GROUP BY price_band ORDER BY MIN(price) NULLS FIRST;
price_band n
Free 3094
Under 5 11613
5 to 15 9046
15 to 30 2367
30 or more 565
%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#4051b5"}}}}%%
xychart-beta
    title "Juegos por franja de precio"
    x-axis ["Free", "Under 5", "5 to 15", "15 to 30", "30 or more"]
    y-axis "Juegos" 0 --> 12000
    bar [3094, 11613, 9046, 2367, 565]

Cinco filas que suman 26,685. Cinco barras en lugar de 199.

Fíjate en la rama WHEN price < 15 THEN '5 to 15'. No dice price >= 5 AND price < 15. No hace falta: una fila solo llega a esa rama si price < 5 no era verdadero. El orden ya hace ese trabajo.

Rómpela dos veces, a propósito

La mejor manera de entender las reglas es estropear la consulta y mirar qué pasa.

Rotura 1 — mueve una rama arriba del todo. Pon WHEN price < 30 THEN '15 to 30' por encima de todas las demás:

price_band n
15 to 30 23026
Free 3094
30 or more 565

Todos los juegos de menos de 30 dólares se detienen en la primera rama. Under 5 y 5 to 15 han desaparecido: 23,026 juegos llevan ahora la etiqueta equivocada.

Rotura 2 — borra la rama Free. Devuelve el orden a su sitio y quita la línea WHEN price IS NULL:

price_band n
Under 5 11613
5 to 15 9046
30 or more 3659
15 to 30 2367

Los 3,094 juegos gratuitos cayeron en ELSE y se unieron a los 565 caros: 3,659 juegos "de 30 o más". Las dos consultas rotas se ejecutan sin un solo error. Es otra vez el error silencioso de la lección de tipos.

Error Qué pasa Cómo detectarlo
Condición amplia colocada la primera Se traga las filas destinadas a las ramas de debajo Faltan franjas en el resultado
Sin rama para NULL Las celdas vacías van a parar a ELSE La franja de ELSE es sospechosamente grande
Una errata en una etiqueta ('5 to 15 ') Se convierte en una categoría propia Una franja de más

Ordenar las categorías en su orden lógico

¿Por qué ORDER BY MIN(price) y no ORDER BY price_band? Porque una etiqueta es texto, y el texto se ordena como un diccionario:

ORDER BY price_band ORDER BY MIN(price) NULLS FIRST
15 to 30 Free
30 or more Under 5
5 to 15 5 to 15
Free 15 to 30
Under 5 30 or more

Alfabéticamente, 15 to 30 va el primero, porque el carácter 1 va antes que 3 y 5. Para obtener el orden lógico ordenas por algo que todavía es un número: el precio más bajo dentro de cada franja. NULLS FIRST pone arriba los juegos gratuitos, que no tienen precio.

Construir owners_tier

La segunda categoría describe la audiencia, usando el owners_min construido en la lección anterior. Tres niveles:

Nivel Regla Significado
Niche owners_min por debajo de 20,000 El rango más bajo: entre 0 y 20,000 propietarios
Mid owners_min por debajo de 1,000,000 De 20,000 hasta un millón
Hit todo lo demás Un millón de propietarios o más

Una columna creada en un SELECT no se puede reutilizar en ese mismo SELECT, así que la expresión de owners_min se escribe otra vez dentro del CASE:

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,

Añadida a la consulta limpia (el bloque ahora se llama step3) y contada con SELECT owners_tier, COUNT(*) AS n FROM step3 GROUP BY owners_tier ORDER BY MIN(owners_min);:

owners_tier n
Niche 17921
Mid 8141
Hit 623

Dos de cada tres juegos de la tabla son Niche. Solo 623 de 26,685 son un Hit.

Ojo: recuerda la unidad

Niche no significa "casi nadie lo compró". Significa que la estimación dice "entre 0 y 20,000 propietarios". Un nivel construido sobre un límite inferior hereda su unidad.

Cruzar dos categorías

Las categorías existen para cruzarse. Con las dos columnas en step3, una sola consulta responde a "¿qué franja de precio produce más éxitos?":

SELECT price_band, COUNT(*) AS games,
       SUM(CASE WHEN owners_tier = 'Hit' THEN 1 ELSE 0 END) AS hits,
       ROUND(100.0 * SUM(CASE WHEN owners_tier = 'Hit' THEN 1 ELSE 0 END) / COUNT(*), 1) AS hit_pct
FROM step3 GROUP BY price_band ORDER BY MIN(price) NULLS FIRST;
price_band games hits hit_pct
Free 3094 182 5.9
Under 5 11613 47 0.4
5 to 15 9046 168 1.9
15 to 30 2367 167 7.1
30 or more 565 59 10.4

El SUM(CASE ... THEN 1 ELSE 0 END) es un truco para contar: cada éxito suma 1, todo lo demás suma 0. La proporción de éxitos pasa del 0.4% entre los juegos de pago más baratos al 10.4% entre los más caros.

Ojo: un precio más alto no hace un éxito

La tabla muestra que los juegos caros son éxitos con más frecuencia. No muestra por qué. Los grandes estudios cobran más y venden más: el tamaño del estudio puede explicar ambas cifras. Que dos cosas se muevan juntas no significa que una cause la otra.

¿Dónde van los límites?

Un límite es el valor donde acaba un intervalo y empieza el siguiente.

¿Por qué < 5 y no <= 5? Mira los precios más comunes:

SELECT price, COUNT(*) FROM games GROUP BY price ORDER BY 2 DESC LIMIT 3;
price count
9.99 3686
4.99 3204
(vacío) 3095

Los precios se amontonan justo por debajo del número redondo: 4.99, 9.99. Así que el límite se pone en el número redondo, y todo el mundo está de acuerdo en de qué lado cae 4.99.

Buena práctica Por qué
Pon los límites en números redondos Fáciles de decir, fáciles de recordar, y los precios se agrupan justo por debajo
Usa una sola dirección en todas partes (< en cada rama) Ningún valor puede caer en dos intervalos ni en ninguno
Nombra los intervalos por sus límites (5 to 15) El lector ve la regla en la etiqueta
Da a NULL su propio intervalo Si no, se esconde dentro de ELSE

Ojo: las franjas no están en los datos

Tú has trazado las líneas en 5, 15 y 30. Mueve una línea y la historia cambia. Un intervalo es una decisión: tiene un autor y debe quedar escrito en el informe.

Y un intervalo tira información a la basura. Dentro de 30 or more, un juego de 39.99 y un programa de 595.99 pasan a ser lo mismo.

Categorías desordenadas

Las categorías de texto también se ensucian. ¿Cuántas editoras (publishers) hay?

SELECT COUNT(DISTINCT publisher) AS as_written,
       COUNT(DISTINCT LOWER(publisher)) AS ignoring_case
FROM games;
as_written ignoring_case
13951 13863

LOWER convierte un texto a minúsculas. Si ignoras las mayúsculas, desaparecen 88 "editoras": eran segundas grafías de una que ya existía. Mira dos de ellas:

SELECT publisher, COUNT(*) FROM games
WHERE LOWER(publisher) IN ('big fish games', 'capcom')
GROUP BY publisher ORDER BY 1;
publisher count
Big FIsh Games 1
Big Fish Games 284
CAPCOM 1
Capcom 33

Para ti, dos editoras. Para una base de datos, cuatro. La misma herramienta las unifica:

CASE WHEN LOWER(publisher) = 'big fish games' THEN 'Big Fish Games'
     ELSE publisher END AS publisher

Eso deja una sola Big Fish Games con 285 juegos. En la tabla de clase no corregimos las 88 grafías: sabemos que existen, y eso va al informe. Conocer un límite y dejarlo escrito también es una decisión.

Pruébalo: franjas y niveles

Abre SQL → SQL Lab, base de datos Dades classe, esquema public. Ejecuta una sentencia cada vez.

Paso 1 — price_band. Ejecuta la consulta de cinco franjas sobre games. Deberías ver cinco filas en el orden Free, Under 5, 5 to 15, 15 to 30, 30 or more, que suman 26,685.

Paso 2 — Rómpela dos veces. Haz los dos cambios descritos arriba, uno cada vez. Antes de arreglar cada uno, apunta los recuentos y una frase que diga adónde fueron a parar los juegos.

Paso 3 — Añade las dos columnas a la consulta limpia. Abre games_step2 en SQL → Saved Queries, cambia el nombre del bloque a step3 y añade tu price_band y la expresión de owners_tier. Cuidado con la coma antes de cada CASE nuevo. Comprueba con el recuento de owners_tier: Niche 17,921, Mid 8,141, Hit 623.

Paso 4 — Guárdala. Pulsa Save, cambia el nombre a games_step3, elige Save as new.

Paso 5 — Cruzarlas. Mantén el bloque WITH step3 y ejecuta la consulta de éxitos. Deberías poder decir qué franja tiene la proporción más alta de éxitos y cuál la más baja.

La base de datos no encuentra la columna price_band

Una columna creada con AS no se puede usar en el WHERE ni en otra columna del mismo SELECT. Solo existe un nivel más arriba, en el SELECT que lee de step3. Por eso la consulta limpia vive dentro de un bloque WITH.

Ideas clave

  • Una categoría hace legible un número: 199 precios se convierten en 5 intervalos. El número se queda en su propia columna, porque una etiqueta no se puede volver a convertir en número.
  • CASE WHEN prueba sus ramas de arriba abajo y se detiene en la primera que se cumple. El orden forma parte de la lógica.
  • ELSE se queda con todo lo demás, incluido NULL. Nombra el caso NULL el primero, con IS NULL.
  • Un orden equivocado o una rama que falta no dan error: comprueba los recuentos de cada franja.
  • Ordena las categorías por un número (ORDER BY MIN(price)), no por la etiqueta, que se ordena alfabéticamente.
  • Los límites son decisión tuya. Ponlos en números redondos, usa una sola dirección y déjalos escritos.
  • Las categorías de texto también se ensucian: Capcom y CAPCOM son dos valores hasta que los unificas.

Comprobación rápida

1. En un CASE WHEN con varias ramas, ¿qué rama toma una fila?
  • La última rama cuya condición se cumple
  • La primera rama cuya condición se cumple, leyendo desde arriba
  • La rama con la condición más estrecha
  • Todas las ramas cuyas condiciones se cumplen

Una fila prueba las ramas en orden y se detiene en la primera que se cumple. Por eso el orden forma parte de la lógica: una condición amplia colocada la primera se traga las filas destinadas a las ramas de debajo.

2. Con CASE WHEN price < 5 THEN 'Under 5' ELSE '5 or more' END, ¿qué etiqueta recibe Dota 2 (precio NULL)?
  • Under 5, porque un juego gratuito cuesta menos de 5
  • 5 or more, porque NULL < 5 no es verdadero y la fila cae en ELSE
  • NULL, porque el precio está vacío
  • La consulta da un error

Una comparación con NULL nunca es verdadera: es desconocida. La fila no coincide con ninguna rama y aterriza en ELSE, que se queda con todo lo demás. Un juego gratuito acaba etiquetado como caro, sin ningún error. La solución es una primera rama WHEN price IS NULL THEN 'Free'.

3. Mueves WHEN price < 30 THEN '15 to 30' al principio de la consulta price_band. ¿Qué pasa?
  • Nada: el orden de las ramas no importa
  • La consulta se detiene con un error
  • 23,026 juegos reciben la etiqueta 15 to 30, y Under 5 y 5 to 15 desaparecen
  • Solo cambian de franja los juegos de más de 30 dólares

Todo precio inferior a 30 también cumple price < 30, así que todas esas filas se detienen en la primera rama. La consulta se ejecuta y devuelve tres franjas en lugar de cinco. Una franja que falta en el resultado es la pista.

4. ¿Por qué el resultado se ordena con ORDER BY MIN(price) NULLS FIRST y no con ORDER BY price_band?
  • Porque ORDER BY no se puede usar sobre una columna CASE
  • Porque ordenar por precio es más rápido
  • Porque las etiquetas son texto y se ordenan alfabéticamente: 15 to 30 iría antes que 5 to 15
  • Porque price_band contiene valores NULL

Una etiqueta es texto, y el texto se ordena como un diccionario: 1 antes que 5. Para obtener el orden lógico ordenas por algo que sigue siendo un número, como el precio más bajo de cada franja.

5. ¿Por qué el límite se escribe price < 5 y no price <= 5?
  • Porque <= no existe en PostgreSQL
  • Porque ningún juego cuesta exactamente 5 dólares
  • Porque los precios se amontonan justo por debajo de los números redondos (4.99, 9.99), así que un límite en el número redondo los separa limpiamente
  • Porque < es más rápido que <=

Los dos precios más comunes son 9.99 y 4.99. Poner cada límite en el número redondo, siempre con <, hace que cada valor caiga en exactamente un intervalo y que todo el mundo esté de acuerdo en de qué lado está 4.99.

6. COUNT(DISTINCT publisher) da 13,951 y COUNT(DISTINCT LOWER(publisher)) da 13,863. ¿Qué te dice la diferencia?
  • 88 editoras no tienen juegos
  • 88 filas tienen la editora vacía
  • 88 valores son solo una grafía distinta, en mayúsculas, de una editora que ya está
  • LOWER borró 88 filas de la tabla

LOWER convierte el texto a minúsculas, así que CAPCOM y Capcom pasan a ser el mismo valor. Las 88 "editoras" que faltan nunca fueron empresas distintas: son categorías desordenadas, una misma cosa escrita de dos formas.

7. Alguien escribe CASE WHEN metascore >= 50 THEN 'Mixed' WHEN metascore >= 75 THEN 'Good' ELSE 'Bad' END. Elige las dos cosas que están mal.
  • Un juego con una puntuación de 90 recibe Mixed, porque >= 50 se cumple primero: la rama >= 75 debe ir encima
  • CASE no se puede usar en una columna numérica
  • Un juego sin puntuación recibe Bad, porque NULL cae en ELSE: necesita su propia rama IS NULL
  • Las etiquetas deben ordenarse alfabéticamente dentro del CASE

Las dos reglas de esta página a la vez. Ejecútala sobre games y ningún juego es Good, mientras que 23,952 son Bad, casi todos juegos que simplemente nunca se valoraron. Corregida (IS NULL primero, luego >= 75, luego >= 50), la tabla tiene 23,838 sin puntuación, 1,457 Mixed, 1,279 Good y solo 114 Bad.

Una de las franjas de hoy no era un precio en absoluto: Free era una suposición sobre lo que significa una celda vacía. ¿Teníamos derecho a hacerla? ¿Y qué pasa con las 23,838 puntuaciones vacías? A continuación: Imputación.