Quitar filas duplicadas parece tan fácil como hacer set(rows). En una lista de clientes real casi nunca lo es, porque cuando la misma persona se registra dos veces casi nunca salen dos filas idénticas. Cambia la fecha del pedido, cambia la ciudad, el email se escribió en mayúsculas la segunda vez. Si esas filas cuentan como duplicadas o no es algo que los datos no pueden decirte. Depende de la clave que compares, y esa elección lo decide todo lo demás: cuántas filas se van, cuál se queda y qué pierdes.

A continuación se toman esas decisiones una a una en Python puro sobre una lista de ejemplo de 412 filas, y se contrastan con el comando Remove Duplicates del Spreadsheet Editor de la aplicación de escritorio de osFoundry, que reduce esa misma elección a un único campo de texto.

La lista

La lista está generada para la demo: cuatro columnas (Email, Name, City, Last order), 374 clientes distintos y 38 filas extra que repiten el email de un cliente anterior. Cada repetición tiene su propia fecha de pedido, y en una de cada tres se volvió a sortear la ciudad, como se vería un segundo registro después de una mudanza. Contando por email, 31 clientes aparecen más de una vez: 25 de ellos dos veces, 5 tres veces y 1 cuatro veces.

import openpyxl ws = openpyxl.load_workbook("en-customers.xlsx").active header, *rows = [tuple(str(c) for c in r) for r in ws.iter_rows(values_only=True)] len(rows) # 412 

Una función, la clave es el argumento

Todas las variantes de abajo son el mismo bucle con otra clave. Se queda con la primera fila que ve para cada clave o, con keep="last", la sobrescribe en su sitio:

def dedupe(rows, key, keep="first"): seen, out = {}, [] for r in rows: k = key(r) if k in seen: if keep == "last": out[seen[k]] = r continue seen[k] = len(out) out.append(r) return out 

Es una búsqueda en un dict por fila, así que es lineal y respeta el orden original de las filas que conserva. pandas hace lo mismo con df.drop_duplicates(subset=[...], keep="first").

Fila completa o una clave

Tres claves sobre las mismas 412 filas:

clave filas conservadas filas eliminadas
fila completa 412 0
Email 374 38
Email + City 387 25

La clave de fila completa no elimina nada. Es lo que hacen por defecto la mayoría de botones de «eliminar duplicados», y en una lista como esta no hace nada sin avisar, porque ninguna repetición comparte la fecha de pedido con la original. El email por sí solo elimina las 38. Email más City elimina 25 y conserva 13 filas que son la misma persona en otra ciudad, lo cual es correcto para una lista de direcciones de entrega e incorrecto para una lista de personas.

El Spreadsheet Editor mete esta elección en un solo campo de su diálogo Remove Duplicates, "Columns (blank = all, e.g. A,C)". En blanco es la clave de fila completa. A es la clave de email.

El diálogo Remove Duplicates del Spreadsheet Editor con la columna A como clave

Al ejecutarlo con A, el recuento de la barra de estado en la columna A pasa de 413 a 375, es decir, 374 emails más el encabezado, el mismo número que conserva la versión en Python.

Qué copia sobrevive

Quedarse con la primera fila es la convención en Excel, en pandas y en este editor, y para tus datos es arbitrario. En esta lista, la primera fila no es el pedido más reciente del cliente en 20 de los 31 clientes repetidos, y 12 de los 31 tienen copias que no coinciden en la ciudad. Tras quitar duplicados quedándose con la primera fila, esos 12 se quedan con la ciudad que se escribiera primero.

Si debe ganar la fila más reciente, ordena antes de quitar duplicados. La ordenación de Python es estable y las fechas ISO se ordenan bien como cadenas, así que basta con esto:

latest = dedupe(sorted(rows, key=lambda r: r[3], reverse=True), key=lambda r: r[0]) len(latest) # 374, each with its latest order 

En una hoja de cálculo, lo equivalente es ordenar por la columna de fecha, de más reciente a más antigua, antes de ejecutar Remove Duplicates.

Normaliza la clave, no los datos

La lista de ejemplo tiene emails limpios. Las listas que rellena la gente, no. Para ver qué se le escapa a una comparación exacta, se reescribieron las 38 filas repetidas en una copia de la lista, rotando entre cinco formas en que se suele teclear un segundo registro: todo en mayúsculas, un espacio al principio, un espacio al final, la primera letra en mayúscula, y la @ y el . en ancho completo por culpa de un método de entrada (@, .). La primera copia de cada cliente se dejó como estaba.

import unicodedata def email_key(e): return unicodedata.normalize("NFKC", e).strip().lower() 
clave filas conservadas filas eliminadas
exacta 409 3
strip().lower() 381 31
NFKC + strip().lower() 374 38

La clave exacta encuentra 3, y son solo copias posteriores del mismo cliente que por casualidad se escribieron igual entre sí. strip().lower() lo detecta todo salvo la puntuación de ancho completo, que en pantalla se ve casi idéntica. NFKC convierte las formas de ancho completo a ASCII y encuentra las 38.

El valor normalizado es solo la clave. La fila que se queda conserva el email exactamente como se escribió, algo importante si la lista vuelve a un sistema que lo guarda.

Dos advertencias. Según la letra del RFC 5321, la parte anterior a la @ distingue mayúsculas de minúsculas, así que pasarla a minúsculas es una suposición práctica que se cumple en los grandes proveedores, no una regla. Y los trucos propios de cada proveedor, como ignorar los puntos en las direcciones de Gmail, fusionan direcciones que otros proveedores tratan como personas distintas; eso va en un paso aparte y deliberado.

Lo que cuesta normalizar

Construyendo un set de claves a partir de 1.000.000 de emails (la lista de ejemplo repetida), con Python 3.12 en un Apple M1, mediana de 5 ejecuciones:

clave tiempo
cadena sin procesar 5 ms
strip().lower() 80 ms
NFKC + strip().lower() 102 ms

El tiempo se va en la normalización, y aun así es una décima de segundo por millón de filas. No hay motivo para saltársela por velocidad.

El orden que funciona

Primero resaltar, luego eliminar. Resaltar los duplicados en la columna clave (en el editor, Conditional Formatting → Highlight Cells Rules → Duplicate Values, que también marca la primera copia) te muestra los grupos antes de borrar nada, y ahí es donde detectas la dirección familiar compartida que no debería fusionarse. Después elige la clave, decide qué copia gana y ordena en consecuencia, normaliza la clave si los datos los tecleó gente, y comprueba el recuento al terminar.

Los scripts detrás de cada número de este artículo usan solo openpyxl y la biblioteca estándar. El Spreadsheet Editor se ejecuta en tu propio equipo, como explica la página sobre el diseño local-first de osFoundry, y la aplicación es gratuita en la página de descarga de osFoundry.