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,COUNTy+cuando se encuentran con unNULL - Sustituir un
NULLconCOALESCEy mantener una columna indicadora que recuerde qué valores se rellenaron - Convertir un valor falso otra vez en
NULLconNULLIF - 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 esNULL. 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:
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)devuelveNULLcuandoxes igual ay, yxen 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,SUMyCOUNT(column)se saltanNULL; la aritmética y las comparaciones conNULLdanNULL. 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
NULLy márcalo cuando no lo sepas o la mayor parte de la columna esté vacía: rellenarmetascorecon 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
COALESCEredondea los precios a la baja - Porque
AVGcuenta cadaNULLcomo 0 - Porque
AVGse salta las filasNULL, 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
COALESCEno funciona sin ella - Porque hace la consulta más rápida
- Porque una vez que el
NULLse 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
COALESCEno 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
NULLpor 0 - Borra las filas donde el tiempo de juego es 0
- Devuelve
NULLdonde 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 enNULLy añadir un indicador comois_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.