Fuzzy Match Merging in Python

Merge multiple sources with fuzzy matching in Python. This tutorial covers the core concept, step-by-step implementation, and practical exercises for combining datasets with imperfect keys.

Focus: merge multiple sources with fuzzy matching

Sponsored

You've got two datasets that should describe the same thing—customers, products, transactions—but the keys don't match. One says "Acme Corp", the other says "Acme Corporation". One says "123 Main St", the other says "123 Main Street". A strict pd.merge() returns zero matches, and you're left staring at rows that should have joined but didn't. This is the pain of merging data from multiple sources in the real world: names get typos, formats vary, and human-generated entries never match perfectly. In this lesson, you'll learn merge multiple sources with fuzzy matching—a technique that uses string similarity to align records even when the keys are imperfect. By the end, you'll be able to combine datasets that a simple merge() would leave behind, saving hours of manual cleanup.

The problem this lesson solves

Real-world data rarely arrives in a clean, consistent format. When you combine data from different systems—a CRM, a billing database, a third-party vendor—the same entity often appears with slightly different spellings, abbreviations, or formatting. A typical scenario: you have a customer list from your sales team and a separate list from your support team. Both have customer names, but one uses "John Smith" and the other uses "J. Smith". A standard pd.merge() on the customer_name column returns nothing because the strings don't match exactly.

The consequence is data loss: rows that should have matched are silently dropped, leaving you with incomplete analysis. If you're merging transaction data with customer profiles, a missed match means revenue attributed to the wrong customer—or no customer at all. This lesson equips you to handle such mismatches by measuring string similarity and using that to join datasets, rather than requiring exact equality.

Core concept / mental model

Fuzzy matching is the process of finding records that are approximately equal to each other, even if they aren't exactly the same. Think of it like a librarian who knows that "J.K. Rowling" and "Joanne Rowling" are the same author, even though the names look different. The computer does this by turning each string into a number that represents how similar it is to another string, using algorithms like Levenshtein distance or token set ratio.

Here's a mental model: imagine each string is a sequence of characters. The similarity score tells you how many edits (insertions, deletions, substitutions) it would take to turn one string into the other. For example, "Acme Corp" vs. "Acme Corporation" requires several insertions, but the ratio is still high because the core words match.

In the context of merging multiple sources, fuzzy matching is applied as a post-processing step after an initial exact merge (or instead of it) to capture the near-matches. The typical workflow looks like this:

  1. Load both datasets into pandas DataFrames.
  2. Generate candidate pairs using one or more key columns.
  3. Compute a similarity score for each pair.
  4. Keep only pairs that exceed a threshold.
  5. Join the data based on those pairs, handling duplicates carefully.

The key is to treat fuzzy matching as a tool for finding potential joins, not as a perfect solution. You still need to review the results and set a sensible threshold to avoid false positives.

How it works step by step

Here is the step-by-step logic behind fuzzy matching, using the RapidFuzz library (a fast alternative to fuzzywuzzy) and pandas.

  1. Prepare your data: Ensure the key columns are strings, lowercase them, and remove whitespace to reduce trivial mismatches.
  2. Extract unique keys from the smaller dataset to compare against the larger one.
  3. For each unique key, compute similarity scores against all possible candidates using a function like fuzz.ratio or fuzz.token_sort_ratio.
  4. Select the best match (or all matches above a threshold) using process.extractOne() or process.extract().
  5. Map the matches back to the original DataFrame using a dictionary or a merge on the original keys.
  6. Handle duplicates: when one key matches multiple records, decide whether to keep all or only the highest score.

The choice of similarity function matters. fuzz.ratio compares the entire string, while fuzz.token_sort_ratio sorts tokens (words) before comparison, which helps when word order varies. For example, "John Smith" and "Smith, John" score low on ratio but high on token_sort_ratio.

The speed of this operation is O(n²) in the worst case, which is why you should always reduce the candidate space—for instance, by pre-filtering on a prefix or using a blocking key like the first letter or the first three characters.

Hands-on walkthrough

Let's put this into practice with a complete example. We'll simulate two datasets: customers and orders, where the customer names are slightly off.

First, install the required libraries (if not already installed):

pip install pandas rapidfuzz

Now, create the datasets and perform the fuzzy merge:

import pandas as pd
from rapidfuzz import fuzz, process

# Sample data
customers = pd.DataFrame({
    'name': ['Acme Corp', 'Globex', 'Initech LLC', 'Umbrella Corp'],
    'customer_id': [101, 102, 103, 104]
})

orders = pd.DataFrame({
    'customer_name': ['Acme Corporation', 'Globex Inc.', 'Initech', 'Sovereign'],
    'order_total': [1500, 800, 1200, 300]
})

# Lowercase and strip for better matching
customers['name_clean'] = customers['name'].str.lower().str.strip()
orders['customer_name_clean'] = orders['customer_name'].str.lower().str.strip()

# Use a dictionary to store matches: order key -> customer key
choices = customers['name_clean'].tolist()
matches = {}
for order_name in orders['customer_name_clean']:
    # Extract the best match above a threshold
    best = process.extractOne(order_name, choices, scorer=fuzz.token_sort_ratio, score_cutoff=70)
    if best:
        matches[order_name] = best[0]

# Create a mapping column in orders
orders['matched_name'] = orders['customer_name_clean'].map(matches)

# Merge the dataframes
merged = orders.merge(customers, left_on='matched_name', right_on='name_clean', how='left')

print(merged[['customer_name', 'order_total', 'name', 'customer_id']])

Expected output:

       customer_name  order_total          name  customer_id
0  Acme Corporation         1500      Acme Corp        101.0
1      Globex Inc.          800         Globex        102.0
2          Initech         1200    Initech LLC        103.0
3        Sovereign          300          NaN          NaN

Notice that "Sovereign" didn't match any customer, so it got NaN—that's a useful flag for manual review.

Now let's see a more advanced scenario: merging multiple sources (three tables) and handling duplicates with a threshold. We'll use a function that returns all matches above a threshold, not just the best one.

from rapidfuzz import process

# Let's assume we have a list of vendor names from two sources
source_a = ['Alpha Industries', 'Beta Corp', 'Gamma Ltd']
source_b = ['Alpha Industry', 'Beta Corporation', 'Gamma']

# Find all matches for each name in source_a against source_b
for name in source_a:
    results = process.extract(name, source_b, scorer=fuzz.token_sort_ratio, score_cutoff=60)
    print(f"{name} -> {results}")

Expected output (scores may vary slightly):

Alpha Industries -> [('Alpha Industry', 95, 0)]
Beta Corp -> [('Beta Corporation', 91, 1)]
Gamma Ltd -> [('Gamma', 80, 2)]

This shows the flexibility of fuzzy matching: you can capture near-matches and then decide how to merge.

Compare options / when to choose what

There are several approaches to merging data with imperfect keys. Here's a comparison:

Method When to use Pros Cons
Exact merge (pd.merge()) Keys are clean and consistent Fast, simple, reliable Fails on any variation
Fuzzy matching (RapidFuzz) Keys have typos or variations Catches near-misses, flexible Slower, need to set threshold
Normalization + exact merge (e.g., lowercase, remove punctuation) Simple formatting differences Quick, no extra dependencies Doesn't handle semantic differences
Record linkage libraries (recordlinkage) Large-scale deduplication Built-in blocking, ML classification Steeper learning curve, heavier

For most cases, RapidFuzz is the best balance of speed and ease. If you're dealing with millions of rows, consider recordlinkage for more sophisticated blocking and indexing.

When to choose fuzzy vs. normalization

If your only issue is formatting (e.g., "Acme Corp" vs "Acme Corp."), normalization (lowercasing, stripping punctuation) might be enough. But if names have typos or abbreviations, fuzzy matching is necessary.

Performance trade-offs

Fuzzy matching is computationally expensive. To keep it fast, reduce the search space by blocking—e.g., only compare records that share the same first letter or same first three characters. RapidFuzz is optimized in C++ and is much faster than older fuzzywuzzy, so prefer it.

Troubleshooting & edge cases

Problem: Fuzzy matches are wrong (false positives).

  • Cause: Threshold is too low, or the scorer is too permissive.
  • Fix: Increase the score_cutoff, or use a stricter scorer like fuzz.ratio instead of token_sort_ratio if word order matters.

Problem: No matches found at all.

  • Cause: The strings are too different, or the scorer isn't appropriate.
  • Fix: Try normalizing the strings first (lowercase, remove extra spaces, strip punctuation) to improve scores. Also consider fuzz.partial_ratio if one string is a substring of another.

Problem: Duplicates in matches.

  • Cause: A single key in one dataset matches multiple rows in the other, and you're keeping all of them.
  • Fix: Use process.extractOne() to pick the best, or deduplicate on the matched key before merging, or keep the highest score using a groupby.

Problem: Memory or speed issues on large data.

  • Cause: Comparing every pair is O(n²).
  • Fix: Apply blocking (e.g., compare only rows with same first letter), or use recordlinkage which implements indexing and efficient algorithms.

Problem: NaN for unmatched rows.

  • Cause: Legitimately no match exists.
  • Fix: Treat unmatched rows as a separate category—review them manually, or flag them for follow-up.

Practice tip: Always visualize a sample of your matches to ensure they look sensible before trusting the automated merge.

What you learned & what's next

You've learned how to merge multiple sources with fuzzy matching in Python, from understanding the core concept of string similarity to implementing it with RapidFuzz and pandas. You can now combine datasets with imperfect keys, handle edge cases like false positives, and choose the right approach for your data. This skill is essential for real-world data analysis, where clean keys are the exception, not the rule.

The next lesson in this track builds on this foundation—likely diving into more advanced data cleaning or handling missing data after joins. You'll be ready to tackle that because you now know how to bring multiple sources together even when their keys don't align perfectly.

Practice recap

Try merging two small datasets of your own—maybe company names from a CSV and a list from an API. Use RapidFuzz to implement a fuzzy merge, then experiment with different thresholds and scorers. Finally, print any unmatched rows and decide how you would handle them in a production workflow.

Common mistakes

  • Setting the similarity threshold too low (e.g., < 60) and getting many false matches, causing incorrect data joins.
  • Forgetting to normalize strings (lowercase, strip whitespace, remove punctuation) before fuzzy matching, which lowers scores and causes missed matches.
  • Using fuzz.ratio when word order varies (e.g., 'John Smith' vs 'Smith, John'), leading to poor scores; use token_sort_ratio instead.
  • Not handling duplicates when a single key matches multiple rows, resulting in inflated merged data or unexpected duplicates.
  • Running fuzzy matching on millions of rows without any blocking, causing performance issues or memory exhaustion.

Variations

  1. Use fuzz.partial_ratio when one string is a substring of another (e.g., 'Acme' vs 'Acme Corporation').
  2. Employ recordlinkage library for large-scale record linkage with built-in indexing and machine-learning classifiers.
  3. Normalize data with pandas (lowercase, strip punctuation) before an exact merge to handle simple formatting differences without fuzzy matching.

Real-world use cases

  • Merging customer records from different CRM systems where names have typos or abbreviations (e.g., 'Bob Smith' vs 'Robert Smith').
  • Combining product catalogs from multiple vendors where the same product has slightly different names or codes.
  • Joining transaction data with company registries when company names differ across sources (e.g., 'Google' vs 'Google LLC').

Key takeaways

  • Fuzzy matching measures string similarity to align records when keys don't match exactly.
  • RapidFuzz's token_sort_ratio handles word order variations and is fast enough for typical datasets.
  • Always normalize strings (lowercase, strip) before fuzzy matching to improve scores.
  • Set a sensible threshold and review matches to avoid false positives.
  • For large datasets, use blocking or recordlinkage to keep fuzzy matching performant.
  • Unmatched rows after fuzzy merge should be flagged and reviewed, not silently ignored.

Sponsored

Sponsored