डुप्लिकेट पंक्तियाँ हटाना सुनने में set(rows) जितना आसान लगता है। असली ग्राहक सूची में ऐसा कम ही होता है, क्योंकि एक ही व्यक्ति दो बार साइन अप करे तो लगभग कभी दो एक जैसी पंक्तियाँ नहीं बनतीं। ऑर्डर की तारीख अलग होती है, शहर बदल चुका होता है, या दूसरी बार ईमेल कैपिटल अक्षरों में टाइप हुआ होता है। इन पंक्तियों को डुप्लिकेट माना जाए या नहीं, यह डेटा खुद नहीं बता सकता। यह इस पर निर्भर करता है कि आप किस key की तुलना करते हैं, और वही चुनाव बाकी सब तय करता है: कितनी पंक्तियाँ हटेंगी, कौन-सी बचेगी, और आप क्या खो देंगे।
नीचे ये फ़ैसले एक-एक करके सादे Python में 412 पंक्तियों की एक उदाहरण सूची पर लिए गए हैं, और उनकी तुलना osFoundry डेस्कटॉप ऐप के Spreadsheet Editor के Remove Duplicates कमांड से की गई है, जो यही चुनाव एक टेक्स्ट फ़ील्ड में देता है।
सूची
सूची डेमो के लिए बनाई गई है: चार कॉलम (Email, Name, City, Last order), 374 अलग-अलग ग्राहक, और 38 अतिरिक्त पंक्तियाँ जो किसी पहले वाले ग्राहक का ईमेल दोहराती हैं। हर दोहराई गई पंक्ति की अपनी ऑर्डर तारीख है, और हर तीसरी पंक्ति के लिए शहर फिर से चुना गया, जैसा घर बदलने के बाद दूसरे साइन-अप में दिखता है। ईमेल के हिसाब से गिनें तो 31 ग्राहक एक से ज़्यादा बार आते हैं: 25 दो बार, 5 तीन बार, 1 चार बार।
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
एक फ़ंक्शन, key उसका आर्ग्युमेंट
नीचे का हर वैरिएंट एक ही लूप है, बस key अलग है। यह हर key के लिए पहली मिली पंक्ति रखता है, या keep="last" देने पर उसी जगह उसे बदल देता है:
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
हर पंक्ति पर एक dict lookup, इसलिए यह लीनियर है और जो पंक्तियाँ बचती हैं उनका मूल क्रम बना रहता है। pandas यही काम df.drop_duplicates(subset=[...], keep="first") से करता है।
पूरी पंक्ति या एक key
उन्हीं 412 पंक्तियों पर तीन key:
| key | बची पंक्तियाँ | हटाई गई पंक्तियाँ |
|---|---|---|
| पूरी पंक्ति | 412 | 0 |
| 374 | 38 | |
| Email + City | 387 | 25 |
पूरी पंक्ति वाली key कुछ भी नहीं हटाती। ज़्यादातर "डुप्लिकेट हटाएँ" बटनों का डिफ़ॉल्ट यही है, और ऐसी सूची पर वह चुपचाप कोई काम नहीं करता, क्योंकि किसी भी दोहराई गई पंक्ति की ऑर्डर तारीख मूल पंक्ति से नहीं मिलती। सिर्फ़ Email सभी 38 हटा देता है। Email के साथ City 25 हटाता है और 13 ऐसी पंक्तियाँ रख लेता है जो असल में एक ही व्यक्ति हैं, बस अलग शहर में। डिलीवरी पतों की सूची के लिए यह सही है, लोगों की सूची के लिए गलत।
Spreadsheet Editor यह चुनाव अपने Remove Duplicates डायलॉग के एक फ़ील्ड में रखता है, "Columns (blank = all, e.g. A,C)"। खाली छोड़ना पूरी पंक्ति वाली key है। A ईमेल वाली key है।
A के साथ चलाने पर कॉलम A के लिए स्टेटस बार की गिनती 413 से 375 हो जाती है, यानी 374 ईमेल और हेडिंग, ठीक उतनी ही जितनी Python वाला वर्ज़न रखता है।
कौन-सी कॉपी बचती है
पहली पंक्ति रखना Excel, pandas और इस एडिटर में चलन है, और आपके डेटा के लिहाज़ से यह मनमाना है। इस सूची में 31 दोहराए गए ग्राहकों में से 20 के लिए पहली पंक्ति उनका सबसे नया ऑर्डर नहीं है, और 31 में से 12 की कॉपियों में शहर अलग-अलग है। पहली पंक्ति वाले dedupe के बाद, इन 12 के पास वही शहर रह जाता है जो सबसे पहले टाइप हुआ था।
अगर सबसे नई पंक्ति को जीतना चाहिए, तो डुप्लिकेट हटाने से पहले sort करें। Python का sort stable है और ISO तारीखें string के रूप में भी सही क्रम में आती हैं, इसलिए बस इतना काफ़ी है:
latest = dedupe(sorted(rows, key=lambda r: r[3], reverse=True), key=lambda r: r[0])
len(latest) # 374, each with its latest order
स्प्रेडशीट में इसका तरीका है Remove Duplicates चलाने से पहले तारीख वाले कॉलम को सबसे नए से पुराने क्रम में sort करना।
डेटा नहीं, key को normalize करें
उदाहरण सूची के ईमेल साफ़ हैं। लोगों की भरी हुई सूचियाँ ऐसी नहीं होतीं। यह देखने के लिए कि सटीक तुलना क्या छोड़ देती है, सूची की एक कॉपी में 38 दोहराई गई पंक्तियों को दोबारा लिखा गया, बारी-बारी से उन पाँच तरीकों से जिनसे दूसरा साइन-अप टाइप होता है: सारे कैपिटल अक्षर, शुरुआत में स्पेस, आख़िर में स्पेस, पहला अक्षर कैपिटल, और इनपुट मेथड से full-width में टाइप हुए @ और . (@, .)। हर ग्राहक की पहली कॉपी को जैसा था वैसा छोड़ा गया।
import unicodedata
def email_key(e):
return unicodedata.normalize("NFKC", e).strip().lower()
| key | बची पंक्तियाँ | हटाई गई पंक्तियाँ |
|---|---|---|
| सटीक | 409 | 3 |
strip().lower() |
381 | 31 |
NFKC + strip().lower()
|
374 | 38 |
सटीक key सिर्फ़ 3 पकड़ती है, और वे भी उसी ग्राहक की बाद वाली कॉपियाँ हैं जो संयोग से आपस में एक ही तरह टाइप हुई थीं। strip().lower() full-width विराम चिह्नों को छोड़कर सब पकड़ लेता है, जो स्क्रीन पर लगभग एक जैसे दिखते हैं। NFKC full-width रूपों को ASCII में बदल देता है और सभी 38 पकड़ लेता है।
Normalize की गई वैल्यू सिर्फ़ key है। जो पंक्ति बचती है उसमें ईमेल ठीक वैसा ही रहता है जैसा लिखा गया था, और यह तब मायने रखता है जब सूची वापस किसी ऐसे सिस्टम में जाती है जो उसे स्टोर करता है।
दो सावधानियाँ। RFC 5321 के अक्षरशः नियम के हिसाब से @ से पहले वाला हिस्सा case-sensitive है, इसलिए उसे lowercase करना एक व्यावहारिक मान्यता है जो बड़े प्रोवाइडर्स पर लागू होती है, कोई नियम नहीं। और प्रोवाइडर-विशेष तरकीबें, जैसे Gmail पतों में डॉट को नज़रअंदाज़ करना, ऐसे पतों को मिला देती हैं जिन्हें दूसरे प्रोवाइडर अलग-अलग लोग मानते हैं; उन्हें एक अलग, सोच-समझकर किया गया स्टेप होना चाहिए।
Normalization की कीमत
1,000,000 ईमेल (उदाहरण सूची को दोहराकर) से keys का set बनाना, Apple M1 पर Python 3.12, 5 रन का median:
| key | समय |
|---|---|
| raw string | 5 ms |
strip().lower() |
80 ms |
NFKC + strip().lower()
|
102 ms |
समय normalization में ही जाता है, और फिर भी यह प्रति दस लाख पंक्तियों पर एक सेकंड का दसवाँ हिस्सा है। स्पीड के लिए इसे छोड़ने की कोई वजह नहीं है।
सही क्रम
पहले highlight करें, फिर हटाएँ। key वाले कॉलम पर डुप्लिकेट highlight करने से (एडिटर में Conditional Formatting → Highlight Cells Rules → Duplicate Values, जो पहली कॉपी को भी मार्क करता है) कुछ भी डिलीट होने से पहले ग्रुप दिख जाते हैं, और यहीं आप वह साझा पारिवारिक पता पकड़ते हैं जिसे मिलाना नहीं चाहिए। फिर key चुनें, तय करें कि कौन-सी कॉपी जीतेगी और उसके हिसाब से sort करें, अगर डेटा लोगों ने टाइप किया है तो key को normalize करें, और बाद में गिनती जाँच लें।
यहाँ के हर नंबर के पीछे की स्क्रिप्ट्स सिर्फ़ openpyxl और standard library इस्तेमाल करती हैं। Spreadsheet Editor आपकी अपनी मशीन पर चलता है, जैसा osFoundry के local-first डिज़ाइन वाला पेज बताता है, और ऐप osFoundry के डाउनलोड पेज से मुफ़्त है।

Top comments (0)