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 | 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_pairsNote: 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.