Saltar al contenido

Indice / pandas: calidad y limpieza / 5.5

Duplicados

Encontrar los duplicados es fácil. Lo difícil es decidir qué cuenta como duplicado en este archivo, y cuál de las copias se queda.

Para qué sirve

Una fila de más infla todos los totales, y no se nota: el archivo tiene el tamaño que esperabas, las columnas están bien, ningún error salta. Simplemente un cliente aparece dos veces y facturó el doble.

Hay dos preguntas, y la segunda es la que se subestima. La primera es qué es un duplicado acá: ¿la fila entera repetida, o dos filas que comparten la clave? La segunda es cuál de las dos se queda, y ahí casi siempre hay una respuesta correcta y varias que parecen funcionar.

ELI5Explicado facil

Tenés dos fichas del mismo cliente. Una dice que su teléfono es 4444-5555 y es de enero; la otra dice 4444-9999 y es de agosto. No son fichas idénticas: son la misma persona anotada dos veces, y una está más al día que la otra.

Si te quedás con cualquiera, la mitad de las veces vas a llamar al número viejo. Y lo peor: vas a tener la cantidad correcta de clientes igual, así que nadie se va a dar cuenta revisando el total.

El ejemplo mínimo

Ejemploduplicated marca las repeticiones, no la primera aparicion
import pandas as pd

pedidos = pd.DataFrame({
    "cliente": ["Ana", "Bruno", "Ana", "Ana"],
    "monto": [100, 250, 100, 300],
})
print(pedidos)

print()
print("duplicated() por fila entera:")
print(pedidos.duplicated().to_list(), "->", int(pedidos.duplicated().sum()), "duplicada")

print()
print("keep='last' marca la otra:")
print(pedidos.duplicated(keep="last").to_list())

print()
print("keep=False marca TODAS las del grupo repetido:")
print(pedidos.duplicated(keep=False).to_list())

print()
print("Por cliente, sin mirar el monto:")
print(pedidos.duplicated(subset=["cliente"]).to_list())
  cliente  monto
0     Ana    100
1   Bruno    250
2     Ana    100
3     Ana    300

duplicated() por fila entera:
[False, False, True, False] -> 1 duplicada

keep='last' marca la otra:
[True, False, False, False]

keep=False marca TODAS las del grupo repetido:
[True, False, True, False]

Por cliente, sin mirar el monto:
[False, False, True, True]
Los tres valores de keep

"first" (por defecto) marca todas menos la primera. "last" marca todas menos la última. False marca todas las del grupo - que es el que sirve para mirar qué está repetido antes de decidir.

Sobre datos de verdad

Exacto contra semántico

EjemploDuplicado exacto y duplicado semantico no son lo mismo
import pandas as pd

libros = pd.read_csv("libros_catalogo.csv")

print("filas totales          :", len(libros))
print("filas identicas enteras:", int(libros.duplicated().sum()))
print("isbn repetidos         :", int(libros.duplicated(subset=["isbn"]).sum()))

print()
repetido = libros[libros.duplicated(subset=["isbn"], keep=False)]
print("Las filas que comparten isbn:")
print(repetido.to_string(index=False))

print()
print("No son identicas: comparten la clave y difieren en lo demas.")
print("Por eso duplicated() sin subset no las encuentra.")
filas totales          : 50
filas identicas enteras: 0
isbn repetidos         : 1

Las filas que comparten isbn:
        isbn                     titulo         autor   anio  precio
978-1000-001          Algoritmos vol. 1 Ximena Molina 2003.0   57.24
978-1000-001 Sistemas operativos vol. 4   Tamara Peña 2003.0   64.43

No son identicas: comparten la clave y difieren en lo demas.
Por eso duplicated() sin subset no las encuentra.

Ninguna fila es idéntica a otra, pero dos comparten el isbn, que es la clave del catálogo. Son dos libros distintos con el mismo código: un error de carga que duplicated() a secas no encuentra, porque compara la fila entera y estas difieren en el título, el autor y el precio.

La conclusión es incómoda y hay que aceptarla: pandas no sabe qué es un duplicado en tu archivo. Eso lo tenés que decidir vos, mirando cuál es la clave.

Cuál se queda

EjemploElegir cual se queda: keep no alcanza, hay que ordenar antes
import pandas as pd

catalogo = pd.read_csv("catalogo_sistema_a.csv")
repetidos = catalogo[catalogo.duplicated(subset=["sku"], keep=False)]
print("Filas con sku repetido:")
print(repetidos.to_string(index=False))

print()
print("keep='first' se queda con la que aparece primero en el archivo:")
print(catalogo.drop_duplicates(subset=["sku"], keep="first")
      .query("sku in ['SKU002', 'SKU007']")[["sku", "precio", "actualizado"]]
      .to_string(index=False))

print()
print("Pero lo que queremos es la mas reciente. Hay que ordenar primero:")
por_fecha = catalogo.sort_values("actualizado", kind="stable")
print(por_fecha.drop_duplicates(subset=["sku"], keep="last")
      .query("sku in ['SKU002', 'SKU007']")[["sku", "precio", "actualizado"]]
      .to_string(index=False))
Filas con sku repetido:
   sku       nombre  precio  stock actualizado
SKU002 Artículo 002  695.67   84.0  2026-08-06
SKU007 Artículo 007   85.34   16.0  2026-07-15
SKU002 Artículo 002   31.50   12.0  2026-08-20
SKU007 Artículo 007   88.00    3.0  2026-08-22

keep='first' se queda con la que aparece primero en el archivo:
   sku  precio actualizado
SKU002  695.67  2026-08-06
SKU007   85.34  2026-07-15

Pero lo que queremos es la mas reciente. Hay que ordenar primero:
   sku  precio actualizado
SKU002    31.5  2026-08-20
SKU007    88.0  2026-08-22

Acá está el punto de la lección. keep="first" se quedó con la fila que aparece primero en el archivo, que resultó ser la versión vieja. El orden del archivo no significa nada: si querés la más reciente, tenés que ordenar primero y recién después deduplicar.

Lo mismo, paso a paso

Paso a pasoDeduplicar por recencia: por que hay que ordenar antes
  1. Cinco filas del catalogo. A simple vista no hay nada raro: todas tienen sus datos completos.
    catalogo = pd.read_csv("catalogo_sistema_a.csv")
  2. duplicated() sin subset no encuentra nada: las filas comparten el sku pero difieren en el precio y en la fecha, asi que no son identicas.
    catalogo.duplicated().sum()   # 0
  3. Con subset=['sku'] aparecen: SKU002 y SKU007 estan dos veces cada uno. Es un duplicado semantico, no exacto.
    catalogo.duplicated(subset=["sku"], keep=False)
  4. drop_duplicates con keep=first se queda con la que aparece primero en el archivo. Para SKU007 esa es la de julio: la vieja.
    catalogo.drop_duplicates(subset=["sku"], keep="first")
  5. El orden del archivo no significa nada. Si queres la mas reciente tenes que decirlo: primero ordenar por la fecha.
    catalogo.sort_values("actualizado", kind="stable")
  6. Y recien ahi deduplicar con keep='last'. Ahora se conservan las de agosto, que es lo que queriamos.
    .drop_duplicates(subset=["sku"], keep="last")
  7. Mira los precios de SKU002: 695.67 contra 31.5. Elegir mal la fila no da error ni cambia la cantidad de filas, solo cambia los numeros. Es el tipo de error que sobrevive a cualquier revision apurada.
    # 62 filas -> 60 filas, y dos precios distintos

Los empates y los ausentes

EjemploEl desempate: que pasa si dos filas empatan
import pandas as pd

catalogo = pd.read_csv("catalogo_sistema_a.csv")
fecha = pd.to_datetime(catalogo["actualizado"], errors="coerce")
print("filas sin fecha de actualizacion:", int(fecha.isna().sum()))

ordenado = catalogo.assign(_f=fecha).sort_values(
    "_f", na_position="first", kind="stable")
unico = ordenado.drop_duplicates(subset=["sku"], keep="last").drop(columns="_f")

print("antes :", len(catalogo), "filas |", catalogo["sku"].nunique(), "sku distintos")
print("despues:", len(unico), "filas |", unico["sku"].nunique(), "sku distintos")

print()
print("Dos decisiones que hay que dejar por escrito:")
print("  na_position='first'  -> una fila sin fecha nunca le gana a una con fecha")
print("  kind='stable'        -> si empatan, gana la que estaba mas abajo")
print("Sin declararlas, dos personas correctas obtienen archivos distintos.")
filas sin fecha de actualizacion: 0
antes : 62 filas | 60 sku distintos
despues: 60 filas | 60 sku distintos

Dos decisiones que hay que dejar por escrito:
  na_position='first'  -> una fila sin fecha nunca le gana a una con fecha
  kind='stable'        -> si empatan, gana la que estaba mas abajo
Sin declararlas, dos personas correctas obtienen archivos distintos.
Dos decisiones que casi nadie escribe, y cambian el resultado

na_position decide si una fila sin fecha puede ganarle a una con fecha. Y kind="stable" decide qué pasa cuando dos filas empatan. Ninguna de las dos da error si las dejás por defecto: simplemente producen otro archivo. Si el enunciado no las fija, dos personas que resuelven bien el ejercicio obtienen resultados distintos.

Dejar constancia

EjemploDeduplicar cambia los conteos: dejalo anotado
import pandas as pd

catalogo = pd.read_csv("catalogo_sistema_a.csv")
antes = len(catalogo)

fecha = pd.to_datetime(catalogo["actualizado"], errors="coerce")
unico = (catalogo.assign(_f=fecha)
         .sort_values("_f", na_position="first", kind="stable")
         .drop_duplicates(subset=["sku"], keep="last")
         .drop(columns="_f")
         .sort_values("sku", kind="stable"))

quitadas = antes - len(unico)
print(f"entraron {antes} filas, salieron {len(unico)}, se quitaron {quitadas}")
print("sku que perdieron una fila:",
      sorted(catalogo.loc[catalogo.duplicated(subset=['sku'], keep=False), 'sku']
             .unique()))

assert unico["sku"].is_unique, "quedaron sku repetidos"
print()
print("El assert es barato y convierte un error silencioso en uno ruidoso.")
entraron 62 filas, salieron 60, se quitaron 2
sku que perdieron una fila: ['SKU002', 'SKU007']

El assert es barato y convierte un error silencioso en uno ruidoso.

Dónde se rompe

(sin error) duplicated() no encuentra los duplicados que ves

Causa: sin subset compara la fila entera, y tus duplicados comparten la clave pero difieren en el resto.

Arreglo: definí la clave y pasala: duplicated(subset=["isbn"]).

(sin error) deduplicaste y los totales dan mal

Causa: te quedaste con la fila equivocada de cada par. La cantidad de filas es la correcta, así que el error no se ve en ningún conteo.

Arreglo: ordenar por el criterio que define «la buena» antes de deduplicar, y dejarlo escrito.

(sin error) drop_duplicates no eliminó nada

Causa: pusiste en subset una columna que difiere entre las filas repetidas - típicamente la fecha. Con eso, las filas dejan de ser duplicadas.

Arreglo: en subset va solo la clave, nunca las columnas que esperás que cambien.

(sin error) quedaron claves repetidas después de deduplicar

Causa: deduplicaste por una columna y verificaste por otra, o la clave real es compuesta (por ejemplo estudiante_id + curso_id).

Arreglo: un assert df["clave"].is_unique después de deduplicar. Cuesta una línea y convierte el error silencioso en uno ruidoso.

Acá hay que elegir

El criterio para quedarse con una fila

No hay un default correcto. Los tres criterios habituales:

  • La más reciente - cuando hay una columna de fecha confiable. Es el caso más común y el más fácil de justificar.
  • La más completa - la que tiene menos ausentes. Útil cuando las copias se complementan, pero hay que definir cómo se cuenta «más completa».
  • Combinar - tomar de cada copia lo que la otra no tiene. Es lo mejor y lo más caro; se justifica cuando perder datos es peor que el trabajo extra.

Elijas el que elijas: escribilo, y anotá cuántas filas se fueron.

Recetario

Duplicados
ExpresiónQué haceCuándo
df.duplicated()Marca repeticiones de fila enteraDuplicados exactos
df.duplicated(subset=["k"])Marca claves repetidasDuplicados semánticos
df.duplicated(keep=False)Marca todas las del grupoPara mirar antes de decidir
df[df.duplicated(subset=["k"], keep=False)]Las filas en conflictoInspeccionarlas de a pares
df.drop_duplicates(subset=["k"])Deja una por claveCuando da igual cuál
df.sort_values("f").drop_duplicates(subset=["k"], keep="last")Deja la más recienteEl caso habitual
sort_values(..., na_position="first")Los ausentes al principioPara que nunca ganen
sort_values(..., kind="stable")Los empates conservan su ordenResultado reproducible
df["k"].is_uniqueboolComo assert, después de deduplicar
df["k"].value_counts().head()Las claves más repetidasVer cuán grave es antes de tocar

Ponete a prueba

1. ¿Qué marca df.duplicated() por defecto?Nivel 1 · Recordar
2. ¿Para qué sirve subset= en duplicated y drop_duplicates?Nivel 1 · Recordar
3.
df = pd.DataFrame({"c": ["Ana","Bruno","Ana","Ana"],
                   "m": [100, 250, 100, 300]})
¿Qué da df.duplicated(subset=["c"]).sum()?Nivel 2 · Predecir la salida
4. Un archivo tiene 62 filas y 60 sku distintos. Corrés drop_duplicates(subset=["sku"]). ¿Cuántas filas quedan?Nivel 2 · Predecir la salida
5. De cada sku repetido querés quedarte con la fila más reciente según actualizado. Las cuatro corren. ¿Cuál lo hace?Nivel 3 · Elegir entre dos casi correctas
6. Al deduplicar por recencia, dos filas del mismo sku tienen la misma fecha. ¿Qué conviene hacer?Nivel 3 · Elegir entre dos casi correctas
7. Este código deduplica un catálogo. La cantidad de filas queda bien, pero el informe de precios da mal. ¿Por qué?
catalogo = pd.read_csv("catalogo_sistema_a.csv")
unico = catalogo.drop_duplicates(subset=["sku"])
print(len(unico))          # 60, correcto
print(unico["precio"].mean())
Nivel 4 · Encontrar el defecto

Prácticas de esta lección