Saltar a contenido

Imputación

Ejercicio imputation-annex.pdf · PDF

Tu profesor está a punto de calcular la media de la clase. Cuatro alumnos, tres notas en el cuaderno de calificaciones: 6, 8 y 10. Tu celda está vacía. ¿Qué debería ir en ella?

¿Un cero? Entonces la media baja de 8 a 6, y acabas de suspender un examen al que faltaste por enfermedad. ¿La media de la clase? Entonces recibes una nota por un trabajo que nadie vio. ¿Nada? Entonces la media se calcula en silencio sin ti. Tres opciones, tres medias distintas, y ninguna es "obviamente la correcta". Cada celda vacía de una tabla plantea esta misma pregunta. Esta página trata de responderla con honestidad.

Qué aprenderás

  • Contar los valores ausentes de una columna y expresarlos como porcentaje de las filas
  • Predecir qué hacen AVG, COUNT y + cuando se encuentran con un NULL
  • Sustituir un NULL con COALESCE y mantener una columna indicadora que recuerde qué valores se rellenaron
  • Convertir un valor falso otra vez en NULL con NULLIF
  • Elegir entre borrar la fila, una constante, un valor central y "dejarlo vacío", y defender la elección en una frase
  • Evitar la trampa: por qué rellenar el 89% de una columna con su media destruye la columna

Un vacío no es un cero

Cuatro lecciones de limpieza, y estas celdas de games siguen vacías:

Columna Celdas vacías Porcentaje de las filas
metascore 23,838 89.3%
price 3,095 11.6%
developer 152 0.6%
publisher 120 0.4%
game 3 0.01%

No puedes limpiar una celda vacía. No hay nada dentro que reparar. Solo puedes decidir qué hacer con ella.

La imputación es decidir qué valor, si hay alguno, sustituye a uno ausente, y dejar escrita esa decisión.

La analogía de toda la página es el cuaderno de calificaciones del profesor del principio: cada decisión que tomes aquí es una de sus filas.

Opción En el cuaderno Es razonable cuando...
Borrar la fila Tachar al alumno de la lista La fila no sirve sin ese valor, y son muy pocas
Una constante Escribir un 0 Sabes qué significa el vacío: el trabajo nunca se entregó
Un valor central Escribir la media de la clase, o la mediana Hay pocos vacíos, y quienes los tienen no son especiales
Dejarlo vacío y marcarlo No escribir nada, añadir una nota "no evaluado" No lo sabes, o la mayor parte de la columna está vacía

Un cero es una nota. Un vacío es una pregunta: ¿por qué está vacío? La respuesta a esa pregunta, no el SQL, decide la opción.

flowchart TB
    Q1{"¿Sabes qué significa<br>el vacío?"}
    Q1 -- "Sí" --> C["<b>Una constante</b><br>escribe lo que significa<br>y añade un indicador"]
    Q1 -- "No" --> Q2{"¿Hay pocos vacíos,<br>repartidos al azar?"}
    Q2 -- "Sí" --> M["<b>Un valor central</b><br>media o mediana<br>y añade un indicador"]
    Q2 -- "No: muchos, o un grupo especial" --> L["<b>Déjalo en NULL</b><br>y añade un indicador"]
    Q1 -- "La fila no sirve sin él" --> D["<b>Borra la fila</b><br>solo si son muy pocas"]

Qué le hace NULL a un cálculo

Antes de rellenar nada, necesitas saber qué hace la base de datos con una celda vacía cuando la dejas en paz. Hace tres cosas distintas, y ninguna te pide permiso.

Operación Se encuentra con un NULL Ejemplo del cuaderno (6, 8, vacío, 10)
COUNT(*) Cuenta la fila igualmente 4 alumnos
COUNT(column) Se lo salta 3 notas
AVG(column), SUM(column) Se lo saltan Media 8: divide 24 entre 3, no entre 4
column + 1, column < 5 El resultado es NULL vacío + 1 = vacío

Velo en la tabla real. La misma columna, dos medias:

SELECT ROUND(AVG(price)::numeric, 2)              AS avg_ignoring_null,
       ROUND(AVG(COALESCE(price, 0))::numeric, 2) AS avg_free_as_zero
FROM games;
avg_ignoring_null avg_free_as_zero
8.95 7.91

¿Cuál es "el precio medio de un juego en Steam"? Depende de lo que signifique el vacío. AVG se saltó en silencio las filas vacías: 8.95 es una suma dividida entre 23,593 juegos, no entre 26,688. Nadie se lo pidió.

Ojo: AVG no cuenta los vacíos como 0

Esta es la suposición equivocada más común. AVG se salta un NULL; no lo trata como cero. Por eso exactamente las dos medias son distintas. Siempre que publiques una media, di sobre cuántas filas se calculó.

COALESCE: el valor y la nota al margen

COALESCE(a, b, ...) devuelve el primero de sus argumentos que no es NULL. Léelo en voz alta como "a, y si está vacío, b".

SELECT game,
       COALESCE(price, 0)::numeric(8,2) AS price,     -- the value we filled in
       (price IS NULL)                  AS is_free    -- the note in the margin
FROM games
WHERE game IN ('Dota 2', 'Half-Life 2');
game price is_free
Dota 2 0.00 true
Half-Life 2 9.99 false

Una columna indicadora (flag column) es una columna extra, verdadera o falsa, que registra algo sobre la fila. Aquí: "este valor estaba vacío en los datos originales".

Los dos siempre viajan en pareja: el valor rellenado y un indicador que recuerda que estaba vacío. Una vez sobrescrito el NULL, nadie puede distinguir un 0 real de uno inventado. El indicador es la nota que el profesor escribe al margen del cuaderno.

Ojo: COALESCE no arregló la tabla

Igual que CAST, cambia el resultado de esta consulta. games no se ha tocado. El relleno solo se vuelve permanente cuando la consulta limpia se convierte en la tabla games_clean, en la próxima lección.

El sustituto debe tener el mismo tipo que la columna. COALESCE(developer, 'Unknown') funciona: texto por texto. COALESCE(developer, 0) se detiene con COALESCE types text and integer cannot be matched. Un error ruidoso, del bueno.

price: una constante que puedes defender

La lección de categorización etiquetó como Free los juegos sin precio. Eso era una suposición. Ahora comprueba si tenías derecho a hacerla. Busca pruebas antes de rellenar nada:

SELECT COUNT(*) FROM games WHERE price = 0;        -- 0
SELECT MIN(price) FROM games;                      -- 0.49
SELECT game, owners FROM games
WHERE price IS NULL ORDER BY number LIMIT 4;
game owners
Team Fortress 2 50,000,000 .. 100,000,000
Dota 2 100,000,000 .. 200,000,000
Divinity: Original Sin (Classic) 1,000,000 .. 2,000,000
BioShock 2 2,000,000 .. 5,000,000

Tres pruebas:

Prueba Qué te dice
Ningún juego cuesta exactamente 0 Los juegos gratuitos deben de estar en otro sitio
El precio más bajo registrado es 0.49 El cero nunca se escribe como precio
Los vacíos empiezan con Team Fortress 2 y Dota 2 Juegos famosos de acceso gratuito (free-to-play) tienen el precio vacío

El conocimiento del dominio es lo que sabes del mundo real que hay detrás de los datos: cómo funciona Steam, qué es un juego free-to-play. Ninguna consulta te lo puede dar.

El conocimiento del dominio dice que un juego sin precio en Steam es de acceso gratuito, y los datos están de acuerdo. Decisión: una constante, 0, más el indicador is_free.

La nota al pie honesta

Lee la lista otra vez. BioShock 2 tampoco tiene precio, y Ori and the Blind Forest, un poco más abajo, tampoco. Ninguno fue nunca un juego gratuito. ¿Rompe eso la regla? La debilita; no la rompe.

La imputación es una suposición escrita en SQL. Un buen analista escribe la suposición junto al número: "precio 0 significa que no hay precio publicado, casi siempre es free-to-play". Esa frase pertenece al informe.

Entonces, ¿qué media publicas? Las dos son respuestas correctas a preguntas distintas:

Pregunta Media Calculada sobre
"¿Cuánto cuesta un juego en Steam?" 7.91 Los 26,688 juegos, los gratuitos como 0
"¿Cuánto cuesta un juego de pago?" 8.95 Los 23,593 juegos con precio

metascore: por qué no se rellena

Ahora el gran agujero: falta el 89.3% de las puntuaciones. La tentación es un valor central.

La media es la suma de los valores dividida entre cuántos hay. La mediana es el valor del medio cuando los ordenas. Las dos describen un valor "típico"; las estudiarás como es debido en Estadística exploratoria.

La metascore media es 71.9 y la mediana es 73. Así que rellena los vacíos con 72, y cuenta:

SELECT COUNT(*) FROM games WHERE metascore = 72;                -- 114
SELECT COUNT(*) FROM games WHERE COALESCE(metascore, 72) = 72;  -- 23952

114 juegos sacaron realmente un 72. Tras el relleno, 23,952 juegos "sacaron" un 72. Mira lo que eso le hace a la columna:

Dos histogramas de metascore. A la izquierda, las 2,850 puntuaciones reales forman una colina entre 20 y 98 con su pico cerca de 80. A la derecha, tras rellenar los vacíos con 72, una única barra roja de 23,952 juegos en 72 hace invisible cualquier otra puntuación.

Lo peligroso: la media apenas se mueve. Era 71.9 y pasa a ser 72.0. La media esconde el daño. Todo lo demás de la columna (cuán dispersas están las puntuaciones, qué juegos son buenos, cuáles malos) se ha aplastado en un solo pico.

Ojo: rellenar con la media no es neutro

Conserva la media y destruye todo lo demás. Nueve de cada diez valores de la columna serían números que te has inventado. Un gráfico de esa columna ya no muestra puntuaciones de la crítica. Muestra tu imputación.

¿Quién falta?

Hay un segundo motivo, y es el más fuerte. Comprueba qué juegos no tienen puntuación. Primero los juegos pequeños:

SELECT (owners LIKE '0%')  AS under_20k_owners,
       COUNT(*)            AS games,
       COUNT(metascore)    AS with_score,
       ROUND(100.0 * COUNT(metascore) / COUNT(*), 1) AS pct_with_score
FROM games GROUP BY 1;
under_20k_owners games with_score pct_with_score
false 8765 2363 27.0
true 17923 487 2.7

Entre los juegos con 20,000 propietarios o más, el 27.0% tiene puntuación. Entre los más pequeños, el 2.7%. Diez veces menos. La crítica reseña los juegos de los que la gente ha oído hablar.

Los valores faltan al azar cuando un vacío es igual de probable en cualquier fila. No faltan al azar cuando los vacíos se amontonan en un grupo concreto.

Aquí se amontonan en los juegos pequeños y desconocidos. Escribir 72 en esas celdas diría "un juego decente, según la crítica" de 23,838 juegos que ningún crítico abrió jamás. Y escribir 0 sería peor: "el peor juego jamás reseñado".

Decisión: déjalo en NULL, y añade el indicador (metascore IS NOT NULL) AS has_metascore. Cualquier gráfico de puntuaciones deberá entonces decir "basado en el 11% de los juegos que fueron reseñados".

price metascore
Vacío 11.6% 89.3%
¿Sabemos qué significa el vacío? Sí: free-to-play No: nadie lo reseñó
¿Falta al azar? No importa: conocemos el valor No: faltan los juegos pequeños
Decisión Constante 0 + is_free Dejar NULL + has_metascore

La misma herramienta SQL, veredicto opuesto.

Columnas de texto: una etiqueta para el vacío

Una columna de texto no puede tener media. Para developer y publisher, dale al vacío su propia categoría, para que GROUP BY lo muestre en lugar de esconderlo:

SELECT COALESCE(developer, 'Unknown') AS developer, COUNT(*) AS games
FROM games GROUP BY 1 ORDER BY games DESC LIMIT 3;
developer games
Unknown 152
Choice of Games 92
KOEI TECMO GAMES CO., LTD. 70

Ojo: Unknown no es un estudio

Según este resultado, el mayor desarrollador de la tabla es Unknown, con 152 juegos. Es una trampa: son 152 juegos distintos de los que no conocemos el desarrollador, no un estudio que hizo 152. La etiqueta mantiene los vacíos visibles. No los convierte en una empresa.

NULLIF: cuando un valor debería ser un vacío

También existe el problema opuesto: un valor que debería estar vacío y no lo está. La auditoría lo encontró en average_playtime: 0 en el 96.5% de los juegos, porque nadie lo midió.

NULLIF(x, y) devuelve NULL cuando x es igual a y, y x en caso contrario. Convierte un valor falso otra vez en un vacío honesto.

SELECT ROUND(AVG(average_playtime)::numeric, 1)            AS with_zeros,
       ROUND(AVG(NULLIF(average_playtime, 0))::numeric, 1) AS zeros_as_null,
       COUNT(NULLIF(average_playtime, 0))                  AS measured
FROM games;
with_zeros zeros_as_null measured
9.1 262.1 922

Una media de 9.1 con los ceros falsos, 262.1 sin ellos, calculada sobre los 922 juegos que sí se midieron. Casi treinta veces mayor. Los ceros arrastraban la media hasta casi nada.

Función Dirección Úsala cuando
COALESCE(x, value) Vacío → valor Sabes qué significa el vacío
NULLIF(x, value) Valor → vacío Un valor es solo un disfraz de "sin datos"

El registro de imputaciones

Un registro de imputaciones es el documento escrito de cada decisión de imputación: columna, cuánto faltaba, qué hiciste y por qué.

Este es el de games. Cada línea es una fila del cuaderno:

Columna Ausentes Decisión Por qué
price 3,095 (11.6%) Constante 0, más indicador is_free Conocimiento del dominio: sin precio en Steam significa free-to-play. Ninguna fila tiene precio 0, y los vacíos incluyen Dota 2 y Team Fortress 2
metascore 23,838 (89.3%) Dejar NULL, más indicador has_metascore Demasiados, y no faltan al azar: el 27.0% de los juegos mayores tiene puntuación, el 2.7% de los más pequeños. Cualquier valor rellenado sería inventado
developer, publisher 152 y 120 (menos del 1%) Constante 'Unknown' Una categoría, no un número. La etiqueta mantiene las filas visibles en cada GROUP BY
game 3 (0.01%) Borrar las filas Un juego sin nombre no se puede identificar ni imputar. 3 filas de 26,688 no cambian nada

Ojo: borrar es el último recurso, no el primero

Borrar toda fila con un vacío es la forma más rápida de "limpiar" y normalmente la equivocada. Si descartas todos los juegos sin metascore, te quedas con 2,850 de 26,688 filas: tus conclusiones describirían un mundo más pequeño y más famoso. Borrar es razonable para 3 filas sin nombre. No lo es para el 89% de una tabla.

Pruébalo: escribe tu registro de imputaciones

Abre SQL → SQL Lab, base de datos Dades classe, esquema public. Cada consulta es un SELECT; no se modifica nada. Ejecuta una sentencia cada vez.

Paso 1 — Mide el agujero. Ejecuta esto para price, y luego repítelo para metascore, developer y publisher:

SELECT COUNT(*)                 AS total_rows,
       COUNT(price)             AS with_value,
       COUNT(*) - COUNT(price)  AS missing,
       ROUND(100.0 * (COUNT(*) - COUNT(price)) / COUNT(*), 1) AS pct_missing
FROM games;

Deberías ver 11.6, 89.3, 0.6 y 0.4.

Paso 2 — price. Ejecuta las tres consultas de pruebas y después la consulta de las dos medias. Escribe qué media publicarías, y para qué pregunta.

Paso 3 — metascore. Ejecuta los dos recuentos con 72 y la consulta de "quién falta". Escribe una frase: ¿por qué 72 es una mentira para un juego que nadie reseñó?

Paso 4 — Columnas de texto. Ejecuta la consulta con COALESCE(developer, 'Unknown'). ¿Quién es el "mayor desarrollador", y por qué es una trampa?

Paso 5 — El registro. Escribe una línea por columna: borrar, constante, valor central, o dejar vacío y marcar.

pct_missing muestra 0 u 11 en lugar de 11.6

División entera. 100 * 3095 / 26688 con números enteros da un número entero. Escribe 100.0 *, con el decimal, antes de dividir, y los decimales vuelven.

function round(double precision, integer) does not exist

ROUND(x, 2) con un número de decimales solo funciona con el tipo numeric. Añade ::numeric antes de redondear: ROUND(AVG(price)::numeric, 2).

Ideas clave

  • La imputación es decidir qué sustituye a un valor ausente. Es una suposición escrita en SQL, así que hay que justificarla y dejarla por escrito.
  • AVG, SUM y COUNT(column) se saltan NULL; la aritmética y las comparaciones con NULL dan NULL. Una media esconde sobre cuántas filas se calculó.
  • COALESCE(x, value) rellena un vacío; empareja siempre con una columna indicadora que recuerde que el valor estaba vacío. NULLIF(x, value) hace lo contrario.
  • Usa una constante cuando sabes qué significa el vacío (price: gratuito significa 0), con conocimiento del dominio y pruebas.
  • Usa un valor central (media o mediana) solo para pocos vacíos que faltan al azar.
  • Déjalo en NULL y márcalo cuando no lo sepas o la mayor parte de la columna esté vacía: rellenar metascore con 72 convierte 114 valores reales en 23,952.
  • Borra filas solo cuando no sirven y son muy pocas.

Comprobación rápida

1. AVG(price) sobre games devuelve 8.95 y AVG(COALESCE(price, 0)) devuelve 7.91. ¿Por qué son distintos?
  • Porque COALESCE redondea los precios a la baja
  • Porque AVG cuenta cada NULL como 0
  • Porque AVG se salta las filas NULL, así que la primera media divide entre menos juegos
  • Porque una de las dos consultas está mal

AVG se salta las celdas vacías: 8.95 se calcula sobre los 23,593 juegos con precio. Con COALESCE, los 3,095 vacíos pasan a ser 0 y entran en el cálculo, así que la suma se divide entre las 26,688 filas. Las dos son respuestas correctas a preguntas distintas.

2. ¿Qué devuelve COALESCE(price, 0) para un juego cuyo precio es 9.99?
  • 0
  • 9.99
  • NULL
  • 9.99 y 0, en dos filas

COALESCE devuelve el primer argumento que no es NULL. El precio existe, así que se devuelve intacto. Solo los precios vacíos pasan a ser 0.

3. ¿Por qué añadimos la columna is_free junto al precio rellenado?
  • Porque COALESCE no funciona sin ella
  • Porque hace la consulta más rápida
  • Porque una vez que el NULL se sustituye por 0, nada más recuerda que el valor estaba vacío
  • Porque los precios no pueden ser 0 en PostgreSQL

Una columna indicadora es la nota al margen del cuaderno. Tras el relleno, un 0 inventado y un 0 real se ven igual. El indicador conserva la diferencia, de modo que cualquiera pueda deshacer o cuestionar la decisión más adelante.

4. ¿Por qué dejamos metascore vacía en lugar de rellenarla con la media, 72?
  • Porque COALESCE no puede rellenar una columna numérica
  • Porque la media de la columna cambiaría mucho
  • Porque falta el 89.3% de la columna y no al azar, así que nueve de cada diez valores serían inventados
  • Porque 72 es una puntuación demasiado baja para la mayoría de los juegos

Rellenar convierte 114 puntuaciones reales de 72 en 23,952, un pico gigante, mientras la media apenas se mueve (de 71.9 a 72.0) y esconde el daño. Y los vacíos no faltan al azar: se amontonan en juegos pequeños que ningún crítico reseñó.

5. ¿Qué hace NULLIF(average_playtime, 0)?
  • Sustituye todos los NULL por 0
  • Borra las filas donde el tiempo de juego es 0
  • Devuelve NULL donde el tiempo de juego es 0, y el tiempo de juego en el resto
  • Cuenta las filas donde el tiempo de juego es 0

NULLIF es lo opuesto de COALESCE: convierte un valor que solo significa "sin datos" otra vez en un vacío honesto. El tiempo de juego medio pasa de 9.1 a 262.1, ahora calculado sobre los 922 juegos que sí se midieron.

6. Tras COALESCE(developer, 'Unknown'), el desarrollador con más juegos es Unknown, con 152. ¿Cuál es la lectura correcta?
  • Un estudio llamado Unknown hizo 152 juegos
  • 152 juegos distintos no tienen desarrollador registrado; no son un solo estudio
  • La consulta está mal y hay que borrarla
  • 152 juegos tienen el desarrollador escrito de dos formas

La etiqueta mantiene los vacíos visibles en un GROUP BY en lugar de esconderlos. Pero es una categoría para "no lo sabemos", no una empresa. Clasificarla como el mayor desarrollador sería un error de interpretación, no de SQL.

7. Una tabla de socios de un gimnasio tiene age vacía en el 4% de las filas y cancellation_date vacía en el 80% de las filas. Elige las dos decisiones que puedes defender.
  • age: rellenar con la mediana de edad y añadir un indicador, porque los vacíos son pocos y nada sugiere que sean un grupo especial
  • cancellation_date: rellenar los vacíos con la fecha más común
  • cancellation_date: dejarla en NULL y añadir un indicador como is_active, porque vacío significa "no se ha dado de baja"
  • Borrar a todo socio con cualquier celda vacía

Dos vacíos, dos historias. Pocos y al azar: un valor central más un indicador es razonable. La fecha de baja vacía significa algo (el socio sigue activo), así que poner una fecha ahí inventaría el 80% de la columna. Borrar tiraría a la basura la mayor parte del gimnasio.

Las celdas vacías ya tienen una decisión y una razón escrita. Has dejado de reparar columnas; es hora de inventar otras nuevas. A continuación: Variables sintéticas, donde por fin se construye la tabla limpia games_clean.