Unit 8 • Lesson 3

When a Merge Goes Wrong

Overview

Duplicate keys multiply rows, and ids written two ways match nothing.

Intermediate 20–25 min

What You Will Learn in This Lesson

By the end of this lesson, you will be able to:

  • Predict the row count of a join before running it.
  • Explain why a duplicate key multiplies rows.
  • Tell a type mismatch, which raises, from text ids that silently match nothing.
  • Have pandas check your assumption with validate.

Why This Matters

These are the two failures that produce a wrong number rather than an error. A duplicated key inflates a total silently; the same id written two ways — "007" and "7", or with a trailing space — empties the result silently. Both are cheap to check for and expensive to discover later, which is the whole argument of this lesson.

Step 1: A key that repeats multiplies

Every matching row on the left pairs with every matching row on the right. Two and two make four, not two.

1

The multiplication

One person, two score rows, two output rows. Sum the scores now and the person counts twice.

import pandas as pd

people = pd.DataFrame({"id": [1], "name": ["Ada"]})
scores = pd.DataFrame({"id": [1, 1], "score": [90, 80]})
out = pd.merge(people, scores, on="id")
print(out)
print(len(people), "x duplicated key ->", len(out), "rows")
2

Check the key before you join

One line, and it answers the question the row count would otherwise raise afterwards.

import pandas as pd

scores = pd.DataFrame({"id": [1, 1, 2], "score": [90, 80, 70]})
print("duplicated keys:", int(scores["id"].duplicated().sum()))
print(scores["id"].value_counts())
3

Let pandas hold you to it

validate states the relationship you believe you have, and raises when the data disagrees — instead of silently multiplying.

import pandas as pd

people = pd.DataFrame({"id": [1], "name": ["Ada"]})
scores = pd.DataFrame({"id": [1, 1], "score": [90, 80]})
try:
    pd.merge(people, scores, on="id", validate="one_to_one")
except Exception as exc:
    print(type(exc).__name__ + ":", exc)

"one_to_one", "one_to_many" and "many_to_one" are the useful three. Writing down which one you expect is also documentation for the next reader.

Mini Practice #1: Predict, then count

Try It Yourself

Write down how many rows you expect before you run this. The right-hand table has a duplicate.

Press Run to see output

Step 2: Keys have to be written the same way

A join compares key values exactly. When the types differ outright pandas refuses, which is the good case. The dangerous case is two text columns that look alike to a person and not to pandas.

1

A type mismatch is loud

An int key against a text key raises ValueError, and the message names the column. Read it: it is telling you the two files disagree about what an id is.

import pandas as pd

a = pd.DataFrame({"id": ["1"], "name": ["Ada"]})
b = pd.DataFrame({"id": [1], "score": [90]})
try:
    pd.merge(a, b, on="id")
except ValueError as exc:
    print("ValueError:", exc)
print(a["id"].dtype, "vs", b["id"].dtype)
2

Make both sides the same type

Convert both id columns before joining, on copies. Converting only one side leaves the mismatch where it was.

import pandas as pd

a = pd.DataFrame({"id": ["1"], "name": ["Ada"]})
b = pd.DataFrame({"id": [1], "score": [90]})

a2, b2 = a.copy(), b.copy()
a2["id"] = a2["id"].astype(str)
b2["id"] = b2["id"].astype(str)
print(len(pd.merge(a2, b2, on="id")), "rows")
3

Text that differs is silent

Once both sides are text, pandas compares them character by character and raises nothing. "1 " and "1" are different keys, and so are "007" and "7" — which is exactly what you get when one file was read as numbers and then converted back to text. No error, no warning, no rows.

import pandas as pd

a = pd.DataFrame({"id": ["1 "], "name": ["Ada"]})
b = pd.DataFrame({"id": ["1"], "score": [90]})
print(len(pd.merge(a, b, on="id")), "rows")

a["id"] = a["id"].str.strip()
print(len(pd.merge(a, b, on="id")), "rows")

Mini Practice #2: Empty for no reason

Try It Yourself

Both id columns are text, so this join raises nothing — and returns nothing. Print the ids from each side to see why, then fix one side so they match.

Press Run to see output

Step 3: When to Use It

Before every merge

Check the key is unique where it should be and has the same dtype on both sides.

pd.merge(a, b, on="id", validate="many_to_one")

After a surprising result

Too many rows means duplicate keys; too few means ids written differently on each side — padding, spaces or case.

When Not to Use It

Do not fix a doubled row count by dropping duplicates afterwards. Find the duplicate keys that caused it, or the numbers built on the merge are still wrong.

End-of-Lesson Exercises

Exercise 1: Join whatever type the ids arrived as

Write safe_merge(left, right) that joins on id after making both id columns text, and returns the number of rows in the result.

Convert both id columns with astype(str) on copies, then merge. Converting only one side leaves the mismatch exactly where it was.

Press Run to try it, then Check my work to mark it.

Exercise 2: Count the duplicate keys first

Write key_report(left, right) returning a three-element list of [duplicated ids on the left, duplicated ids on the right, rows an inner join would produce]. Checking before joining is the point.

df["id"].duplicated().sum() counts the repeats on each side. Then merge and take len() — two duplicates each side give four rows, which is the multiplication this exercise is about.

Press Run to try it, then Check my work to mark it.