Indu Learning Platform
Course/Unit 4 · Data engineering for master data
Lesson 4-2

Matching and deduplication

How duplicate detection really works, from normalisation and blocking to fuzzy scores and steward review, so you can design and explain it.

Watch firstCheuk Ting Ho - Fuzzy Matching Smart Way of Finding Similar Names Using Fuzzywuzzy · EuroPython ConferenceOpen on YouTube ↗

Matching is a pipeline

'Sharma Steel Traders Pvt Ltd', 'SHARMA STEEL TRADERS PRIVATE LIMITED' and 'Sharma Steels' in three source systems: exact name matching finds none, and tax ID matching only works where the ID is filled and correct. So deduplication is a pipeline, each stage shrinking noise or cost until human review is small.

Step through · 1 of 6
Normalisecase, legal formsBlockPIN / city keyCompare pairsrapidfuzzScoreweighted fieldsThresholdauto / review / noSteward reviewmerge + survivorship
Make values comparable: lower case, remove punctuation, expand '&' to 'and', strip legal forms like Pvt Ltd, Limited, LLP.
Every enterprise tool, including MDG's duplicate check, follows some version of this.

Deterministic rules first, fuzzy second

Apply exact identifier rules first, remove those pairs, then spend fuzzy effort on the rest. Beware identifiers that only look strong: a shared-service bank account, a dummy tax ID like AAAAA0000A, a head-office phone used by fifty branches. Your profile tells you which identifiers to trust.

DeterministicSame GSTIN / VAT ID = same entitySame bank account + countryEasy to explain to auditorsMisses typos and missing IDsFuzzy / probabilistic'L&T Ltd' ≈ 'Larsen and Toubro Limited'Tolerates typos and word orderNeeds thresholds and reviewScores must be tuned per fieldvs
Exact rules for identifiers, fuzzy scores for names and addresses.

Normalise, then block

Normalisation turns many 'near duplicates' into exact ones. Keep originals; add normalised columns beside them. Then block: n records give n × (n − 1) / 2 pairs, so 1 million partners mean about 500 billion comparisons. Comparing only within a shared key (PIN, name prefix) fixes that; use two or three keys so a PIN typo does not hide a duplicate.

ApproachRecordsPairs to compareRiskAll vs all1,000≈ 500,000None, just slow at scaleAll vs all1,000,000≈ 500 billionNot feasibleBlock on PIN code1,000,000tens of millionsMisses PIN typosPIN + name prefix (union)1,000,000somewhat moreFewer misses
Blocking trades a small risk of missed matches for a huge drop in comparisons.
Python
import re

LEGAL_FORMS = r"\b(private|pvt|limited|ltd|llp|inc|gmbh|co|company)\b"

def norm_name(s) -> str:
    if not isinstance(s, str):
        return ""
    s = s.lower().replace("&", " and ")
    s = re.sub(r"[^a-z0-9 ]", " ", s)       # drop punctuation
    s = re.sub(LEGAL_FORMS, " ", s)           # drop legal forms
    return " ".join(s.split())                # collapse spaces

df["name_n"] = df["NAME_ORG1"].map(norm_name)
# Sharma Steel Traders Pvt. Ltd.  ->  sharma steel traders
# Larsen & Toubro Limited         ->  larsen and toubro

Scoring pairs with rapidfuzz

rapidfuzz scorers return 0–100, and the scorer matters. process.extract(query, choices, scorer=..., limit=3) returns the best candidates from a list; process.cdist scores whole lists at once for large volumes.

Name A (normalised)Name B (normalised)token_sortVerdictlarsen and toubrolarsen and toubro100Auto-matchacme componentscomponents acme100Auto-matchbharat packagingbharath packagings94Reviewshree ganesh logisticssri ganesh logistics90Reviewkaveri agro foodskaveri textiles38No match
Real fuzz.token_sort_ratio scores on synthetic names. fuzz.ratio gives only 67 for the Acme pair.

Try it: pick the scorer

Try it · Match

Match each rapidfuzz function to the job it does best.

Pick an item on the left, then its match on the right.

Combine fields and set thresholds

Two different 'Ganesh Traders' in different cities both score 100 on name, so weight several fields and let an exact identifier override. Set two thresholds: above the upper one auto-match, between them steward review, below ignore. Start conservative.

≥ 92 auto-match184080–91 review231070–79 inspect sample3950< 70 ignored61200
Candidate pairs by score band in a synthetic 48,000-supplier run. The review band is the steward workload.
Python
from rapidfuzz import fuzz

AUTO, REVIEW = 92, 80

def pair_score(r1, r2) -> float:
    if r1["TAX_NUMBER"] and r1["TAX_NUMBER"] == r2["TAX_NUMBER"]:
        return 100.0                       # exact identifier wins
    name = fuzz.token_sort_ratio(r1["name_n"], r2["name_n"])
    street = fuzz.token_set_ratio(r1["street_n"], r2["street_n"])
    city = 100.0 if r1["city_n"] == r2["city_n"] else 0.0
    return 0.6 * name + 0.3 * street + 0.1 * city

def verdict(s: float) -> str:
    return "auto-match" if s >= AUTO else "review" if s >= REVIEW else "no-match"

Try it: duplicate or not?

Try it · Sort

Using the rules above (exact ID overrides, AUTO 92, REVIEW 80), sort each pair.

Duplicate (auto-match)
Steward review
Not a duplicate
Click an item, then a bucket (or drag it).

Steward review, survivorship and MDG

The steward compares both records, confirms or rejects, and decides survivorship field by field. On a migration you keep a cross-reference from old keys to the surviving BP. MDG's duplicate check and MDG consolidation use the same steps; Python is where you prototype and tune rules before configuring them there.

Candidate pairscore 86Side-by-side viewConfirm / rejectSurvivorshipfield by fieldKey cross-referenceold → new BP
Each steward decision is a labelled example for tuning thresholds.

Try it: tuning the grey zone

Try it · What would you do?

After mock 2, stewards reject 70% of pairs in the 80–91 review band and complain the work list is too long. What do you do?

Remember

  • Matching is a pipeline: normalise, block, compare, score, threshold, review.
  • Normalise first, block on more than one key, and pick the rapidfuzz scorer per field with exact identifiers overriding.
  • Steward decisions are labelled data for tuning thresholds and MDG match configuration.

Check yourself

Why do matching pipelines use blocking?
Which rapidfuzz scorer gives 100 for 'acme components' vs 'components acme'?
Two records have the same GSTIN but quite different names. What should a well-designed rule do?
What is the main risk of blocking only on PIN code?