excel-fuzzy-duplicates-match

piotrek / excel-fuzzy-duplicates-match

Read a file ~/Documents/companies-with-duplicates.xlsx

Thinking

Clarifying the file path

I need to clarify the exact filepath that's been provided, so I think loading and inspecting it first might be a smart approach. It's essential to ensure I have the right details before moving on. I'll get that sorted out to avoid any confusion later! Let's go ahead and check the specifics before proceeding further. This way, I can be more confident in what I'm doing.

</>Load excel data
import pandas as pd

file_path = "~/Documents/companies-with-duplicates.xlsx"
df = pd.read_excel(file_path)

print(f"Shape: {df.shape}")
columns = df.columns.tolist()
print(f"Columns ({len(columns)}):", columns if len(columns) <= 100 else columns[:100] + ["..."])
df.head()
Shape: (40, 10)
Columns (10): ['Customer_ID', 'Company_Name', 'Contact_Name', 'Email', 'Phone', 'Street_Address', 'City', 'Country', 'Annual_Spend_USD', 'Signup_Date']
Customer_ID Company_Name Contact_Name Email Phone Street_Address City Country Annual_Spend_USD Signup_Date
0 C025 ACME Industrial Solutions Robert Clark robert.clark@acmeindustrial.com +1 312 555 0174 600 West Adams Street Chicago USA 74200 2024-05-09
1 C023 GreenLeaf Foods Sp. z o.o. Marek Kowalski marek.kowalski@greenleaf.pl +48 22 555 09 91 ul. Długa 14 Warsaw Poland 46300 2023-07-12
2 C031 Nova Retail Group Olivia Parker olivia.parker@novaretail.com +1 646 555 0108 355 Madison Avenue New York USA 95700 2024-11-19
3 C037 Red Fox Marketing Agency Nina Patel nina.patel@redfoxmarketing.co.uk +44 131 555 0201 16 George Street Edinburgh UK 27600 2024-07-30
4 C028 Blue Peak Consulting A. Fischer a.fischer@blue-peak.de 089 555 0114 Leopoldstrasse 28 München DE 39100 2025-02-05

The workbook contains 40 customer-company records across 10 fields, including company and contact details, location, annual spend, and signup date. The available columns support identifying duplicate company records using identifiers and contact information.

how many exact duplicate rows are there, and are any Customer_ID, Company_Name,
Email or Phone values repeated

Thinking

Figuring out data duplication

I need to provide an answer with code that focuses on exact duplicate rows and repeats. I’m thinking I can create a single table or chart as a default output. It might be useful to produce both a printed scalar and a DataFrame showing the counts of duplicates. I want to be clear about duplicating rows, meaning I need to account for excluding the first instance and showing counts for all duplicates. I'll combine this into one code solution.

This checks fully identical rows and summarizes repeated values in the requested fields.

</>Find repeated values
check_columns = ["Customer_ID", "Company_Name", "Email", "Phone"]

exact_duplicate_rows = int(df.duplicated().sum())
rows_in_exact_duplicate_groups = int(df.duplicated(keep=False).sum())

print(f"Exact duplicate rows (excluding the first occurrence): {exact_duplicate_rows}")
print(f"Rows that belong to exact-duplicate groups: {rows_in_exact_duplicate_groups}")

repeated_frames = []

for column in check_columns:
    counts = (
        df[column]
        .value_counts(dropna=False)
        .rename_axis("Value")
        .reset_index(name="Occurrences")
    )
    counts = counts[counts["Occurrences"] > 1].assign(Field=column)
    repeated_frames.append(counts[["Field", "Value", "Occurrences"]])

repeated_values = pd.concat(repeated_frames, ignore_index=True)

display(repeated_values.sort_values(["Field", "Occurrences"], ascending=[True, False]))
Exact duplicate rows (excluding the first occurrence): 0
Rows that belong to exact-duplicate groups: 0
Field Value Occurrences

There are 0 exact duplicate rows in the 40-row dataset.

None of the requested fields contain repeated values:
- Customer_ID
- Company_Name
- Email
- Phone

install rapidfuzz and find pairs of company names that are similar but not identical

</>Find similar company names
%pip install -q rapidfuzz

from itertools import combinations
from rapidfuzz.fuzz import token_sort_ratio

company_names = df["Company_Name"].dropna().astype(str).drop_duplicates().tolist()

similar_pairs = [
    {
        "Company_Name_1": name_1,
        "Company_Name_2": name_2,
        "Similarity_Score": round(token_sort_ratio(name_1, name_2), 1),
    }
    for name_1, name_2 in combinations(company_names, 2)
    if name_1 != name_2 and token_sort_ratio(name_1, name_2) >= 70
]

similar_company_pairs = (
    pd.DataFrame(similar_pairs)
    .sort_values("Similarity_Score", ascending=False)
    .reset_index(drop=True)
)

similar_company_pairs
Note: you may need to restart the kernel to use updated packages.
Company_Name_1 Company_Name_2 Similarity_Score
0 Mueller Technik GmbH Muller Technik GmbH 97.4
1 Evergreen Solar Technology Evergreen Solar Technologies 92.6
2 Baltic Logistic SA Baltic Logistics S.A. 92.3
3 GreenLeaf Foods Sp. z o.o. Green Leaf Foods Sp z oo 92.0
4 Northwind Analytcs Limited Northwind Analytics Ltd 89.8
5 ACME Industrial Solutions Acme Industrial Solution 85.7
6 Red Fox Marketing Agency RedFox Marketing Agency 85.1
7 Nova Retail Group NOVA Retail Grp. 72.7
8 Blue Peak Consulting BluePeak Consulting GmbH 72.7
9 Horizon Bio Labs Horizon Biolabs 71.0

RapidFuzz identified 10 non-identical company-name pairs with similarity scores of at least 70%.

The strongest likely duplicate or variant pairs are:

Company name 1 Company name 2 Similarity
Mueller Technik GmbH Muller Technik GmbH 97.4%
Evergreen Solar Technology Evergreen Solar Technologies 92.6%
Baltic Logistic SA Baltic Logistics S.A. 92.3%
GreenLeaf Foods Sp. z o.o. Green Leaf Foods Sp z oo 92.0%
Northwind Analytcs Limited Northwind Analytics Ltd 89.8%
ACME Industrial Solutions Acme Industrial Solution 85.7%
Red Fox Marketing Agency RedFox Marketing Agency 85.1%

The remaining three pairs are lower-confidence matches, at 71.0%–72.7%, but likely reflect formatting, abbreviation, or spacing differences.

show the matches with Contact_Name, Email, Phone, City, Annual_Spend_USD and
Signup_Date for both rows side by side

Thinking

Thinking about code structure

I need to work on the code next. It focuses on a single table, merging similar pairs or calculating relevant pairs. I should use IDs instead of names since they’re likely unique. The goal is to display them side by side, using the merge function twice. I’ll ensure the output isn’t comma-separated, and check if I might encounter issues with exact uniqueness during the merge. Enhancing it by indexing records directly seems better. Let’s get coding!

</>Merge match details
details = [
    "Contact_Name", "Email", "Phone", "City",
    "Annual_Spend_USD", "Signup_Date"
]

left_details = df[["Company_Name"] + details].rename(
    columns={
        "Company_Name": "Company_Name_1",
        **{column: f"{column}_1" for column in details},
    }
)

right_details = df[["Company_Name"] + details].rename(
    columns={
        "Company_Name": "Company_Name_2",
        **{column: f"{column}_2" for column in details},
    }
)

matches_with_details = (
    similar_company_pairs
    .merge(left_details, on="Company_Name_1", how="left")
    .merge(right_details, on="Company_Name_2", how="left")
)

display(matches_with_details)
Company_Name_1 Company_Name_2 Similarity_Score Contact_Name_1 Email_1 Phone_1 City_1 Annual_Spend_USD_1 Signup_Date_1 Contact_Name_2 Email_2 Phone_2 City_2 Annual_Spend_USD_2 Signup_Date_2
0 Mueller Technik GmbH Muller Technik GmbH 97.4 J. Weber j.weber@muellertechnik.de 040 555 6623 HAMBURG 81850 2023-04-30 Jonas Weber jonas.weber@muller-technik.de +49 40 555 6621 Hamburg 82100 2023-04-28
1 Evergreen Solar Technology Evergreen Solar Technologies 92.6 Dave Ross d.ross@evergreen-solar.com (512) 555-0170 AUSTIN 88350 2025-03-27 David Ross david.ross@evergreensolar.com +1 512 555 0168 Austin 88600 2025-03-25
2 Baltic Logistic SA Baltic Logistics S.A. 92.3 P. Zielinski p.zielinski@baltic-logistics.pl 58 555 77 23 Gdansk 104100 2023-02-18 Piotr Zieliński piotr.zielinski@balticlogistics.pl +48 58 555 77 21 Gdańsk 104500 2023-02-16
3 GreenLeaf Foods Sp. z o.o. Green Leaf Foods Sp z oo 92.0 Marek Kowalski marek.kowalski@greenleaf.pl +48 22 555 09 91 Warsaw 46300 2023-07-12 M. Kowalski m.kowalski@green-leaf.pl 22 555 09 93 Warszawa 46100 2023-07-14
4 Northwind Analytcs Limited Northwind Analytics Ltd 89.8 Sara Miller s.miller@northwind-analytics.com 020 5555 0189 LONDON 58150 2024-01-20 Sarah Miller sarah.miller@northwindanalytics.com +44 20 5555 0188 London 58400 2024-01-18
5 ACME Industrial Solutions Acme Industrial Solution 85.7 Robert Clark robert.clark@acmeindustrial.com +1 312 555 0174 Chicago 74200 2024-05-09 Rob Clark r.clark@acme-industrial.com (312) 555-0176 CHICAGO 73950 2024-05-10
6 Red Fox Marketing Agency RedFox Marketing Agency 85.1 Nina Patel nina.patel@redfoxmarketing.co.uk +44 131 555 0201 Edinburgh 27600 2024-07-30 N. Patel n.patel@redfox-marketing.co.uk 0131 555 0203 EDINBURGH 27450 2024-08-01
7 Nova Retail Group NOVA Retail Grp. 72.7 Olivia Parker olivia.parker@novaretail.com +1 646 555 0108 New York 95700 2024-11-19 O. Parker o.parker@nova-retail.com 646-555-0110 New York 95400 2024-11-20
8 Blue Peak Consulting BluePeak Consulting GmbH 72.7 A. Fischer a.fischer@blue-peak.de 089 555 0114 München 39100 2025-02-05 Anna Fischer anna.fischer@bluepeak.de +49 89 555 0112 Munich 38900 2025-02-03
9 Horizon Bio Labs Horizon Biolabs 71.0 Marie Dubois marie.dubois@horizonbio.fr +33 4 91 55 10 22 Marseille 69100 2023-11-27 M. Dubois m.dubois@horizon-bio.fr 04 91 55 10 24 MARSEILLE 68900 2023-11-29

The side-by-side table contains all 10 fuzzy company-name matches, ranked from 71.0% to 97.4% similarity, with the requested contact, email, phone, city, annual spend, and signup-date fields for each matching row. The displayed output is truncated, so the specific values are not visible in the available notebook output.

how many pairs would I find with a threshold of 75 instead of 70

Thinking

Determining score threshold

I'm focusing on finding the step count threshold to determine likely scores. It seems like 7 could be the right number, particularly since scores equal to or greater than 75 include the first score (85.1) and the next (72.7). But I might need to use the notebook for execution. The user asked for exact numbers, so I should confirm that 7 is the output based on scores shown.

With a similarity threshold of 75, you would find 7 matching company-name pairs.

what is the total Annual_Spend_USD now, and what would it be if each pair counted
once keeping the row with the earlier Signup_Date

This calculates the current total and the deduplicated total by retaining the earlier Signup_Date within every 70%-or-higher matched pair. It also flags any ties in signup dates.

</>Calculate deduplicated spend total
df["Signup_Date"] = pd.to_datetime(df["Signup_Date"], errors="coerce")

pair_dates = (
    similar_company_pairs
    .merge(
        df[["Company_Name", "Signup_Date"]].rename(
            columns={"Company_Name": "Company_Name_1", "Signup_Date": "Signup_Date_1"}
        ),
        on="Company_Name_1",
        how="left",
    )
    .merge(
        df[["Company_Name", "Signup_Date"]].rename(
            columns={"Company_Name": "Company_Name_2", "Signup_Date": "Signup_Date_2"}
        ),
        on="Company_Name_2",
        how="left",
    )
)

remove_names = set(
    pair_dates.apply(
        lambda row: row["Company_Name_2"]
        if row["Signup_Date_1"] <= row["Signup_Date_2"]
        else row["Company_Name_1"],
        axis=1,
    )
)

current_total = df["Annual_Spend_USD"].sum()
deduplicated_total = df.loc[
    ~df["Company_Name"].isin(remove_names), "Annual_Spend_USD"
].sum()

print(f"Current total Annual_Spend_USD: {current_total:,.2f} USD")
print(f"Total after counting each matched pair once: {deduplicated_total:,.2f} USD")
print(f"Rows excluded: {len(remove_names)}")
print(f"Pairs with tied or missing Signup_Date: {int((pair_dates['Signup_Date_1'] == pair_dates['Signup_Date_2']).sum())}")
Current total Annual_Spend_USD: 2,225,500.00 USD
Total after counting each matched pair once: 1,542,150.00 USD
Rows excluded: 10
Pairs with tied or missing Signup_Date: 0

The current total Annual_Spend_USD is 2,225,500.00 USD.

If each of the 10 matched company-name pairs is counted once, retaining the record with the earlier Signup_Date, the total becomes 1,542,150.00 USD.

That is a reduction of 683,350.00 USD. No matched pairs had tied or missing signup dates.

merge each pair into one row, keeping the row with the earlier Signup_Date, then
save as companies-deduplicated.xlsx

Thinking

Planning code for saving data

I need to create a code for saving data. The current pairs seem simple enough with no overlaps, but I need to ensure I'm constructing a deduplicated DataFrame and saving it, possibly in the current directory. I could mention that 10 items were removed and perhaps 58 remain, but I'm not sure. It’s important to avoid mutating the target and to include safeguards for the output path and shape. I’ll use the same removed names state for saving.

This retains the earlier-signup record from each matched pair, removes the 10 later records, and saves the result as companies-deduplicated.xlsx.

</>Deduplicate and save companies
deduplicated_df = (
    df.loc[~df["Company_Name"].isin(remove_names)]
    .sort_values("Signup_Date")
    .reset_index(drop=True)
)

output_path = "companies-deduplicated.xlsx"
deduplicated_df.to_excel(output_path, index=False)

print(f"Saved {len(deduplicated_df)} rows to: {output_path}")
print(f"Annual_Spend_USD total: {deduplicated_df['Annual_Spend_USD'].sum():,.2f} USD")
Saved 30 rows to: companies-deduplicated.xlsx
Annual_Spend_USD total: 1,542,150.00 USD

Each of the 10 matched company-name pairs was consolidated by retaining the row with the earlier Signup_Date. The resulting dataset contains 30 rows and has been saved as:

companies-deduplicated.xlsx

The retained records have a combined Annual_Spend_USD of 1,542,150.00 USD.

Made with MLJAR
Explore more conversationsMore from piotrek