Limpieza: unidades y duplicados¶
Ejercicio cleaning-units-duplicates-annex.pdf · PDF
En un juego de crafteo, dos objetos iguales se apilan en un solo hueco. De vez en cuando, dos objetos parecen idénticos y aun así se niegan a apilarse. Mismo icono, mismo nombre, dos huecos. Algo que no ves los hace distintos.
Los datos hacen esto todo el rato. Un espacio que no es un espacio. Tres juegos que se llaman exactamente igual y no son el mismo juego. Un número que parece "cuánta gente tiene este juego" y no lo es. En esta página aprendes a encontrar la diferencia oculta antes de fusionar, separar o borrar nada.
Qué aprenderás
- Descubrir qué carácter hay realmente dentro de un texto con
ASCII, y explicar qué es un espacio de no separación - Extraer un número de un rango de texto con
SPLIT_PARTyREGEXP_REPLACE, y convertirlo - Decir qué unidad tiene una columna, y por qué un número sin su unidad es un nuevo tipo de suciedad
- Distinguir un duplicado verdadero (la misma fila dos veces) de un duplicado aparente (el mismo nombre, otro juego)
- Eliminar duplicados verdaderos con
DISTINCToROW_NUMBER - Evitar la trampa: por qué "una fila por nombre de juego" borra en silencio 77 filas reales
Se ven idénticos, no se apilan¶
El inventario de suciedad dejó dos líneas para hoy: owners es texto y contiene un rango, y 74 nombres de juegos aparecen más de una vez. Las dos esconden el mismo tipo de trampa, así que mantenemos una sola analogía en toda la página: dos objetos que se ven iguales y no se apilan.
Esta lección continúa con normalizar: un tipo, un formato y una unidad por columna, y una fila por cada cosa real.
flowchart LR
subgraph A ["<b>Parte 1</b>: la columna owners"]
direction TB
A1["'10,000,000 .. 20,000,000'<br>texto, un rango"] --> A2["<b>owners_min</b><br>10000000, un número"]
end
subgraph B ["<b>Parte 2</b>: nombres repetidos"]
direction TB
B1["74 nombres aparecen<br>más de una vez"] --> B2["¿Duplicado verdadero,<br>duplicado aparente<br>o zona gris?"]
end
A --> B
El espacio que no es un espacio¶
owners dice 10,000,000 .. 20,000,000. Queremos el primer número. La herramienta es SPLIT_PART(text, separator, n): corta un texto en un separador y devuelve el trozo número n. El separador parece obvio: espacio, dos puntos, espacio.
SELECT owners, SPLIT_PART(owners, ' .. ', 1) AS first_part
FROM games WHERE game = 'Half-Life 2';
| owners | first_part |
|---|---|
| 10,000,000 .. 20,000,000 | 10,000,000 .. 20,000,000 |
No se cortó nada. first_part es otra vez el texto entero. Comprueba cuántas filas contienen "espacio, dos puntos, espacio":
SELECT COUNT(*) FROM games WHERE owners LIKE '% .. %'; -- 0
Cero. Ni una fila. Los espacios los ves en la pantalla, y la base de datos dice que no están.
Toma la huella dactilar¶
Un código de carácter es el número que un ordenador guarda para cada carácter. La letra, la cifra o el espacio que ves es solo la forma en que ese número se dibuja en pantalla.
ASCII(character) muestra el código. Toma el carácter que está justo antes de los puntos y compáralo con un espacio escrito con tu teclado:
SELECT ASCII(SUBSTR(owners, POSITION('..' IN owners) - 1, 1)) AS mystery,
ASCII(' ') AS normal_space
FROM games WHERE game = 'Half-Life 2';
| mystery | normal_space |
|---|---|
| 160 | 32 |
Tu teclado escribe el carácter 32. El archivo contiene el carácter 160.
Un espacio de no separación (non-breaking space) es un carácter que se ve exactamente como un espacio pero le dice a un programa "no partas la línea aquí". Su código es 160 (en el estándar Unicode,
U+00A0; en una página web se escribe ).
Esta tabla se recogió de un sitio web, Steam Spy, y la decoración de la página vino con los datos. Mismo aspecto, objeto distinto: no se apilan, así que el separador nunca encajó.
| Espacio normal | Espacio de no separación | |
|---|---|---|
| Código | 32 | 160 |
| En pantalla | Un hueco | El mismo hueco |
| Se escribe con | La barra espaciadora | Normalmente se copia de una página web o de un documento |
Lo quita TRIM |
Sí | No |
Coincide con ' ' en una consulta |
Sí | No |
Un carácter oculto es cualquier carácter que no puedes distinguir a simple vista: un espacio de no separación, un tabulador, un salto de línea pegado al final de un valor.
Cuidado: TRIM no quita todos los espacios
TRIM solo quita el espacio normal, código 32, y solo en los dos extremos. El código 160 sobrevive intacto. Cuando dos valores parecen iguales y la base de datos insiste en que son distintos, deja de mirar la pantalla y pide los códigos.
De un rango de texto a un número¶
Ahora construye el número, paso a paso. Cada fallo enseña algo.
Corta por lo que es de fiar. Los dos puntos son caracteres normales. Corta ahí y mide el trozo:
SELECT SPLIT_PART(owners, '..', 1) AS part,
LENGTH(SPLIT_PART(owners, '..', 1)) AS len,
LENGTH('10,000,000') AS expected
FROM games WHERE game = 'Half-Life 2';
| part | len | expected |
|---|---|---|
| 10,000,000 | 11 | 10 |
Once caracteres donde tú cuentas diez. El de más es el espacio de no separación, todavía pegado al final.
Falla a propósito. Usa las herramientas de la lección anterior: TRIM al espacio, REPLACE a las comas, cast.
SELECT REPLACE(TRIM(SPLIT_PART(owners, '..', 1)), ',', '')::bigint FROM games;
ERROR: invalid input syntax for type bigint: "10000000 "
Mira dentro de las comillas, después de las cifras: todavía hay un hueco. TRIM no lo quitó. Un error ruidoso, por suerte.
Quédate solo con las cifras. REGEXP_REPLACE(text, pattern, new, 'g') reemplaza cada trozo del texto que encaje con un patrón. El patrón [^0-9] significa "cualquier cosa que no sea una cifra": comas, espacios de cualquier tipo y el carácter extraño que aparezca el año que viene.
SELECT REGEXP_REPLACE(SPLIT_PART(owners, '..', 1), '[^0-9]', '', 'g')::bigint AS owners_min,
COUNT(*) AS n
FROM games GROUP BY owners_min ORDER BY owners_min;
| owners_min | n |
|---|---|
| 0 | 17923 |
| 20000 | 3126 |
| 50000 | 1733 |
| 100000 | 1408 |
| 200000 | 1329 |
| 500000 | 546 |
| 1000000 | 325 |
| 2000000 | 211 |
| 5000000 | 50 |
| 10000000 | 30 |
| 20000000 | 3 |
| 50000000 | 3 |
| 100000000 | 1 |
Trece valores, ordenados como números, de 0 a 100,000,000. Y el ranking que era un disparate en la auditoría ahora tiene sentido: ordenado por owners_min de mayor a menor, Dota 2 está arriba con 100,000,000, seguido de tres juegos con 50,000,000 (Counter-Strike: Global Offensive, Team Fortress 2 y PLAYERUNKNOWN'S BATTLEGROUNDS).
| Pieza de la expresión | Qué hace |
|---|---|
SPLIT_PART(owners, '..', 1) |
Se queda con lo que hay antes de los dos puntos |
REGEXP_REPLACE(..., '[^0-9]', '', 'g') |
Borra todo lo que no es una cifra |
'g' |
"Global": todas las coincidencias, no solo la primera |
::bigint |
Convierte las cifras en un número entero grande |
Cuidado: no olvides la 'g'
Sin 'g', solo se quita el primer carácter que no es una cifra. Entonces el cast falla con invalid input syntax for type bigint: "10000,000 ": una coma menos, pero la otra y el espacio oculto siguen ahí.
Un número necesita su unidad¶
Una unidad es lo que cuenta o mide un número: dólares, minutos, personas, kilómetros. Un número sin su unidad se puede leer de varias maneras, y al menos una de ellas es errónea.
Entonces, ¿cuál es la unidad de owners_min? Es tentador decir "personas que tienen el juego". No lo es.
| Podrías leer | Lo que es en realidad |
|---|---|
| "10,000,000 personas tienen Half-Life 2" | "Al menos 10,000,000, según una estimación" |
| "0 personas tienen este juego" | "Entre 0 y 20,000, en algún punto" |
| Un recuento exacto | El límite inferior de un rango estimado |
17,923 juegos tienen owners_min = 0. Eso no significa que nadie los comprara. Significa "entre 0 y 20,000". Llamamos a la columna owners_min, y no owners, justo por eso: el nombre lleva la unidad.
Lo mismo vale para el resto de la tabla. price está en dólares estadounidenses, no en euros. Nada en la columna lo dice: solo lo sabes porque la descripción del conjunto de datos lo indica. Apúntalo.
Un error de unidades que costó una sonda espacial
El 23 de septiembre de 1999, la NASA perdió el contacto con el Mars Climate Orbiter al llegar a Marte. La investigación encontró la causa en un archivo de números. El software del fabricante, Lockheed Martin, daba el impulso de los propulsores en libras-fuerza segundo. El software de navegación del Jet Propulsion Laboratory de la NASA leyó esos mismos números como newtons-segundo, la unidad métrica. No faltaba ningún valor y ninguna consulta falló. La nave, que había costado unos 125 millones de dólares, entró demasiado baja y fue destruida.
Todos los números eran "correctos". Solo la unidad estaba mal, y nadie la había escrito junto al número.
Cuidado: esta suciedad la podríamos haber creado nosotros
Limpiar puede crear suciedad. Si hubiéramos llamado owners a la columna nueva y la hubiéramos puesto en un gráfico titulado "Jugadores por juego", habríamos convertido un rango honesto en un recuento falso. Ponle a la columna el nombre de lo que es, y dilo en el informe.
Verdadero, aparente y gris: tres tipos de "duplicado"¶
Ahora los 74 nombres que aparecen más de una vez. ¿Borramos las copias? Antes de contestar, hay tres casos distintos.
Un duplicado verdadero es una fila idéntica a otra en todas las columnas: el mismo registro dos veces. Un duplicado aparente solo comparte algunas columnas, como el nombre, y en realidad es otra cosa.
| Caso | En games |
Qué hacer |
|---|---|---|
| Duplicado verdadero | Todas las columnas idénticas: la misma fila dos veces. Causa típica: un archivo cargado dos veces | Eliminar las copias. No se pierde nada |
| Duplicado aparente | Mismo nombre, distinto desarrollador y fecha: Dark Matter son tres juegos distintos |
Conservarlos todos. Solo se parecen |
| Zona gris | Mismo nombre y desarrollador, dos fechas: Penumbra Overture, 2007 y 2009. ¿Una reedición? |
Una decisión. La nuestra: conservar, y apuntarlo |
De los 74 nombres repetidos, 32 tienen desarrolladores distintos y 42 tienen el mismo desarrollador con fechas distintas. Ni un solo par comparte a la vez el nombre y la fecha.
¿Tiene games duplicados verdaderos?¶
SELECT DISTINCT * conserva una copia de cada fila totalmente idéntica. Cuenta antes y después:
SELECT COUNT(*) FROM games; -- 26688
SELECT COUNT(*) FROM (SELECT DISTINCT * FROM games) AS t; -- 26688
El mismo número. games no tiene duplicados verdaderos. Los 74 nombres repetidos son todos duplicados aparentes o zona gris.
El accidente: un archivo subido dos veces¶
¿Quién no ha pulsado Subir dos veces alguna vez? Puedes simularlo sin tocar la tabla. UNION ALL pega dos resultados uno debajo de otro, copias incluidas:
WITH twice AS (SELECT * FROM games UNION ALL SELECT * FROM games)
SELECT COUNT(*) FROM twice; -- 53376
53,376 filas: cada juego dos veces. Ahora DISTINCT se gana el sueldo:
WITH twice AS (SELECT * FROM games UNION ALL SELECT * FROM games)
SELECT COUNT(*) FROM (SELECT DISTINCT * FROM twice) AS t; -- 26688
De vuelta a 26,688. Las copias eran duplicados verdaderos, así que quitarlas no perdió nada.
Cuidado: DISTINCT depende de las columnas que seleccionas
DISTINCT solo fusiona filas idénticas en todas las columnas seleccionadas. SELECT DISTINCT * pregunta "¿se repite la fila entera?". SELECT DISTINCT game pregunta "¿qué nombres existen?" y devuelve 26,611 filas, fusionando los tres Dark Matter en uno. Dos preguntas muy distintas con la misma palabra clave.
Eliminar duplicados con una clave¶
Una clave es la columna, o el grupo de columnas, que dice "esto es la misma cosa". Dos filas con la misma clave son el mismo objeto real.
A veces las copias no son perfectamente idénticas (una tiene un espacio de más, otra se actualizó después), así que DISTINCT * no las detecta. Entonces eliges una clave y numeras las filas dentro de cada grupo. ROW_NUMBER() hace la numeración: 1, 2, 3... empezando de nuevo en cada grupo.
WITH twice AS (SELECT * FROM games UNION ALL SELECT * FROM games),
numbered AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY game, release_date, developer ORDER BY number) AS rn
FROM twice
)
SELECT rn, COUNT(*) FROM numbered GROUP BY rn ORDER BY rn;
| rn | count |
|---|---|
| 1 | 26688 |
| 2 | 26688 |
PARTITION BY nombra la clave: nombre, fecha y desarrollador juntos. Cada juego recibió un número de fila 1 y una copia con número de fila 2. Quédate con WHERE rn = 1 y las copias desaparecen.
flowchart TB
Q{"¿La fila entera es<br>idéntica a otra?"}
Q -- "Sí" --> T["<b>Duplicado verdadero</b><br>quitar la copia:<br>DISTINCT o rn = 1"]
Q -- "No" --> K{"¿Coinciden todas las<br>columnas de la clave?"}
K -- "No: otro desarrollador, otra fecha" --> A["<b>Duplicado aparente</b><br>otra cosa distinta: conservarla"]
K -- "Unas sí, otras no" --> G["<b>Zona gris</b><br>decidir, y apuntarlo"]
La clave descuidada¶
Ahora el error que todo el mundo tiene la tentación de cometer: "un juego es su nombre". Usa PARTITION BY game a secas, sobre la tabla original:
WITH numbered AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY game ORDER BY number) AS rn
FROM games
)
SELECT rn, COUNT(*) FROM numbered GROUP BY rn ORDER BY rn;
| rn | count |
|---|---|
| 1 | 26611 |
| 2 | 75 |
| 3 | 2 |
Quedarse solo con rn = 1 deja 26,611 filas. Esa "limpieza" borraría 77 filas, y ninguna es una copia. Dos de los tres juegos Dark Matter están entre ellas, y también el segundo Penumbra Overture. Sin error. Sin aviso. La tabla simplemente se hace más pequeña, y parece más ordenada que antes.
| Clave | Filas conservadas | Filas perdidas | Veredicto |
|---|---|---|---|
La fila entera (DISTINCT *) |
26,688 | 0 | Correcto: no había duplicados verdaderos |
game, release_date, developer |
26,688 | 0 | Correcto: esta combinación es única en games |
game a secas |
26,611 | 77 | Incorrecto: borra juegos reales que comparten nombre |
¿Cómo sé que mi clave es correcta?
Demuéstralo. Agrupa por la clave y pide los grupos con más de una fila: GROUP BY game, release_date, developer HAVING COUNT(*) > 1. En games devuelve nada, así que la clave identifica cada fila. Si devuelve filas, míralas antes de decidir nada.
Pruébalo: owners_min y la caza de duplicados¶
Abre SQL → SQL Lab, base de datos Dades classe, esquema public. Ejecuta una sentencia cada vez: selecciónala y pulsa Run.
Paso 1 — Encuentra al culpable. Ejecuta el SPLIT_PART con ' .. ', el recuento con LIKE y la consulta ASCII. Deberías obtener el texto entero, luego 0, y luego 160 y 32.
Paso 2 — Construye owners_min. Ejecuta la consulta que mide (11 contra 10), la que falla a propósito y la del REGEXP_REPLACE. Deberías ver 13 filas ordenadas como números.
Paso 3 — Añádelo a la consulta de limpieza. Abre games_step1 en SQL → Saved Queries y haz clic en su nombre. En el bloque WITH, sustituye el simple owners por la expresión REGEXP_REPLACE AS owners_min, y renombra el bloque a step2. Compruébalo con SELECT game, owners_min FROM step2 ORDER BY owners_min DESC LIMIT 4;. Dota 2 tiene que estar arriba.
Paso 4 — Guárdalo con otro nombre. Pulsa Save, cambia el nombre a games_step2 y elige Save as new. Elegir Update sobrescribiría games_step1.
Paso 5 — Caza duplicados. Ejecuta los dos recuentos con DISTINCT *, las consultas twice y, por último, la clave descuidada. Deberías poder decir cuántas filas reales borraría PARTITION BY game, y nombrar una de ellas.
El separador ' .. ' me ha funcionado. ¿Por qué?
Probablemente copiaste el valor de owners de la cuadrícula de resultados y lo pegaste en tu consulta. Los espacios de no separación vinieron con el pegado, así que tu separador también contiene el código 160. Funciona, pero nadie que lea tu consulta puede ver por qué. Cortar por '..' es la versión de la que te puedes fiar.
Ideas clave¶
- Cada carácter se guarda como un código de carácter. Un espacio de no separación (código 160) se ve como un espacio (código 32) y es un carácter distinto:
TRIMy' 'no lo tocan. SPLIT_PARTcorta un texto en un separador;REGEXP_REPLACE(..., '[^0-9]', '', 'g')deja solo las cifras; después conviertes.- Un número necesita su unidad.
owners_mines el límite inferior de una estimación, no un recuento:0significa "entre 0 y 20,000". - Un duplicado verdadero es la fila entera repetida. Un duplicado aparente solo comparte un nombre. Entre medias hay una zona gris que necesita una decisión escrita.
gamesno tiene duplicados verdaderos:DISTINCT *devuelve las mismas 26,688 filas.- Una clave dice qué hace que dos filas sean la misma cosa. Con
ROW_NUMBER() OVER (PARTITION BY key), las copias son las filas conrnmayor que 1. - Una clave descuidada borra datos reales sin ningún error: una fila por nombre de juego perdería 77 filas.
Comprobación rápida¶
1. SPLIT_PART(owners, ' .. ', 1) devuelve el texto entero, sin cortar. ¿Por qué?
- Porque
SPLIT_PARTno funciona con columnas de texto - Porque los dos puntos son caracteres especiales en SQL
- Porque los espacios alrededor de los puntos son espacios de no separación, código 160, no el espacio normal que escribiste
- Porque la columna está vacía para ese juego
Tu teclado escribe el espacio con el código de carácter 32. El archivo contiene el código 160, el espacio de no separación, que vino de la página web de la que se recogieron los datos. Se ven igual y son caracteres distintos, así que el separador nunca coincide.
2. ¿Qué afirmación sobre TRIM es verdadera?
- Quita todos los espacios de un texto
- Quita solo los espacios normales de los dos extremos
- Quita cualquier carácter que no sea una cifra
- Quita los espacios de no separación pero conserva los normales
TRIM corta el espacio normal, código 32, al principio y al final de un texto. Deja en paz los espacios del medio, y no reconoce el código 160. Para eliminar todo lo que no sea una cifra necesitas REGEXP_REPLACE con el patrón [^0-9].
3. ¿Qué hace la 'g' en REGEXP_REPLACE(part, '[^0-9]', '', 'g')?
- Hace que el resultado sea un número
- Ignora las letras mayúsculas
- Reemplaza todas las coincidencias, no solo la primera
- Agrupa el resultado como
GROUP BY
'g' significa global. Sin ella solo se quita el primer carácter que no es una cifra, así que 10,000,000 se convierte en 10000,000 y el cast a bigint falla con un error.
4. Un juego tiene owners_min = 0. ¿Qué te dice eso?
- Que nadie tiene el juego
- Que falta el valor
- Que el juego tiene entre 0 y 20,000 propietarios, según una estimación
- Que el juego es gratis para jugar
owners_min es el límite inferior de un rango, y el rango es una estimación. Su unidad no es "personas que tienen el juego". Leerlo como un recuento exacto sería suciedad creada por nosotros mismos.
5. SELECT COUNT(*) FROM games y SELECT COUNT(*) FROM (SELECT DISTINCT * FROM games) AS t devuelven los dos 26,688. ¿Qué demuestra eso?
- Que ninguna fila entera se repite: la tabla no tiene duplicados verdaderos
- Que ningún nombre de juego se repite
- Que la columna
numberes una clave - Que
DISTINCTno funciona en tablas grandes
DISTINCT * fusiona solo las filas idénticas en todas las columnas. Recuentos iguales significan que no hay duplicado verdadero. No dice nada sobre los nombres: 74 nombres siguen compartidos por juegos distintos.
6. Dos filas se llaman Penumbra Overture, ambas de Frictional Games, una con fecha de 2007 y otra de 2009. ¿Qué es esto?
- Un duplicado verdadero: borrar una de ellas
- Un duplicado aparente: claramente dos juegos sin relación
- Una zona gris: necesita una decisión, y la decisión hay que apuntarla
- Un valor atípico
Mismo nombre y mismo desarrollador, pero dos fechas: quizá una reedición, y los datos no lo dicen. No es un duplicado verdadero (las filas difieren) y tampoco es obviamente otro juego. En la tabla de clase la decisión es conservar las dos y dejarlo registrado.
7. Un compañero quiere eliminar duplicados de una tabla de alumnos conservando una fila por nombre de pila. Elige las dos cosas que le dirías.
- Primero comprueba si se repite alguna fila entera, con
DISTINCT * - Una fila por nombre de pila es seguro mientras la consulta no dé error
- Un nombre de pila no es una clave: personas distintas lo comparten, así que se borrarían alumnos reales sin ningún aviso
- Usa
TRIMen los nombres y los duplicados desaparecerán
Es otra vez la clave descuidada. En games, una fila por nombre borra 77 filas reales y no aparece ningún error. Una clave debe identificar una cosa real; demuéstralo con un GROUP BY ... HAVING COUNT(*) > 1 que no devuelva nada.
Cada columna ya tiene un tipo, un formato y una unidad que sabes nombrar. Pero 199 precios distintos y 13 valores de propietarios siguen siendo difíciles de leer de un vistazo. A continuación: Categorización, donde los números se convierten en etiquetas como Común, Raro y Épico.