DEV Community

Cover image for Come rimuovere i duplicati da una lista clienti in Python quando nessuna riga è identica

Come rimuovere i duplicati da una lista clienti in Python quando nessuna riga è identica

Togliere le righe duplicate sembra una questione di set(rows). Su una vera lista clienti raramente lo è, perché la stessa persona che si registra due volte quasi mai produce due righe identiche. La data dell'ordine è diversa, la città è cambiata, la seconda volta l'email è stata scritta in maiuscolo. Se quelle righe siano duplicati o no, i dati non te lo dicono. Dipende dalla chiave che confronti, e quella scelta decide tutto il resto: quante righe spariscono, quale resta e cosa perdi.

Qui sotto le decisioni vengono prese una alla volta in Python puro su una lista di esempio di 412 righe, e confrontate con il comando Remove Duplicates dello Spreadsheet Editor dell'app desktop di osFoundry, che riduce la stessa scelta a un unico campo di testo.

La lista

La lista è generata per la demo: quattro colonne (Email, Name, City, Last order), 374 clienti diversi e 38 righe in più che ripetono l'email di un cliente già presente. Ogni ripetizione ha la sua data d'ordine, e per una su tre la città è stata estratta di nuovo, come apparirebbe una seconda registrazione dopo un trasloco. Contando per email, 31 clienti compaiono più di una volta: 25 due volte, 5 tre volte e 1 quattro volte.

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
Enter fullscreen mode Exit fullscreen mode

Una funzione, la chiave è l'argomento

Tutte le varianti qui sotto sono lo stesso ciclo con una chiave diversa. Tiene la prima riga che incontra per ogni chiave oppure, con keep="last", la sovrascrive al suo posto:

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
Enter fullscreen mode Exit fullscreen mode

Un lookup in un dict per riga, quindi è lineare e mantiene l'ordine originale delle righe che tiene. pandas fa lo stesso con df.drop_duplicates(subset=[...], keep="first").

Riga intera o una chiave

Tre chiavi sulle stesse 412 righe:

chiave righe tenute righe rimosse
riga intera 412 0
Email 374 38
Email + City 387 25

La chiave sulla riga intera non rimuove niente. È il comportamento predefinito della maggior parte dei pulsanti «rimuovi duplicati», e su una lista come questa non fa nulla senza dirtelo, perché nessuna ripetizione ha la stessa data d'ordine dell'originale. L'email da sola le rimuove tutte e 38. Email più City ne rimuove 25 e tiene 13 righe che sono la stessa persona in un'altra città, il che va bene per una lista di indirizzi di consegna e male per una lista di persone.

Lo Spreadsheet Editor mette questa scelta in un solo campo della finestra Remove Duplicates, "Columns (blank = all, e.g. A,C)". Vuoto è la chiave sulla riga intera. A è la chiave sull'email.

La finestra Remove Duplicates dello Spreadsheet Editor con la colonna A come chiave

Eseguendolo con A, il conteggio nella barra di stato sulla colonna A passa da 413 a 375, cioè 374 email più l'intestazione, lo stesso numero che tiene la versione in Python.

Quale copia sopravvive

Tenere la prima riga è la convenzione in Excel, in pandas e in questo editor, e per i tuoi dati è una scelta arbitraria. In questa lista la prima riga non è l'ordine più recente del cliente per 20 dei 31 clienti ripetuti, e 12 dei 31 hanno copie che non concordano sulla città. Dopo una deduplicazione che tiene la prima riga, quei 12 restano con la città scritta per prima.

Se deve vincere la riga più recente, ordina prima di deduplicare. L'ordinamento di Python è stabile e le date ISO si ordinano correttamente come stringhe, quindi basta questo:

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

In un foglio di calcolo l'equivalente è ordinare per la colonna della data, dalla più recente, prima di eseguire Remove Duplicates.

Normalizza la chiave, non i dati

La lista di esempio ha email pulite. Le liste compilate da persone no. Per vedere cosa si perde un confronto esatto, le 38 righe ripetute sono state riscritte in una copia della lista, alternando cinque modi in cui viene digitata una seconda registrazione: tutto maiuscolo, uno spazio all'inizio, uno spazio alla fine, l'iniziale maiuscola, e la @ e il . digitati a larghezza piena da un metodo di input (@, .). La prima copia di ogni cliente è rimasta com'era.

import unicodedata

def email_key(e):
    return unicodedata.normalize("NFKC", e).strip().lower()
Enter fullscreen mode Exit fullscreen mode
chiave righe tenute righe rimosse
esatta 409 3
strip().lower() 381 31
NFKC + strip().lower() 374 38

La chiave esatta ne trova 3, e sono solo copie successive dello stesso cliente che per caso erano state digitate allo stesso modo tra loro. strip().lower() prende tutto tranne la punteggiatura a larghezza piena, che a schermo sembra quasi identica. NFKC riporta le forme a larghezza piena in ASCII e le trova tutte e 38.

Il valore normalizzato è solo la chiave. La riga che resta conserva l'email esattamente come era stata scritta, cosa che conta se la lista torna in un sistema che la memorizza.

Due avvertenze. Alla lettera dell'RFC 5321, la parte prima della @ distingue maiuscole e minuscole, quindi convertirla in minuscolo è un'assunzione pratica che regge con i grandi provider, non una regola. E i trucchi specifici di un provider, come ignorare i punti negli indirizzi Gmail, uniscono indirizzi che altri provider considerano persone diverse; vanno in un passaggio separato e consapevole.

Quanto costa normalizzare

Costruendo un set di chiavi da 1.000.000 di email (la lista di esempio ripetuta), Python 3.12 su un Apple M1, mediana di 5 esecuzioni:

chiave tempo
stringa grezza 5 ms
strip().lower() 80 ms
NFKC + strip().lower() 102 ms

Il tempo se ne va nella normalizzazione, ed è comunque un decimo di secondo per milione di righe. Non c'è motivo di saltarla per guadagnare velocità.

L'ordine che funziona

Prima evidenzia, poi rimuovi. Evidenziare i duplicati sulla colonna chiave (nell'editor, Conditional Formatting → Highlight Cells Rules → Duplicate Values, che segna anche la prima copia) ti mostra i gruppi prima di cancellare qualsiasi cosa, ed è lì che noti l'indirizzo condiviso da una famiglia che non va unito. Poi scegli la chiave, decidi quale copia vince e ordina di conseguenza, normalizza la chiave se i dati li hanno digitati delle persone, e alla fine controlla il conteggio.

Gli script dietro ogni numero di questo articolo usano solo openpyxl e la libreria standard. Lo Spreadsheet Editor gira sulla tua macchina, come spiega la pagina sul design local-first di osFoundry, e l'app è gratuita dalla pagina di download di osFoundry.

Top comments (0)