merge and join

pandas · Practical · 15 min

Real data lives in several tables; joining them correctly is essential and a classic source of silent errors.

`pd.merge(left, right, on="key", how="inner")` (or left.merge(right, ...)) combines rows from two DataFrames whose keys match, exactly like a SQL join.

The how argument:

  • "inner" (default): only keys present in both
  • "left": all rows of left, with NaN where right has no match; "right" the mirror image
  • "outer": all keys from both sides
  • "cross": every combination (no key)

When key columns have different names, use left_on="cust_id", right_on="id". To join on the index, use left_index=True / right_index=True, or the shortcut `left.join(right)`, which joins on the index of right (a left join by default). Overlapping non-key column names get suffixes=("_x", "_y"); set meaningful ones.

The biggest danger is unexpected duplicate keys. If a key appears twice on each side, the result has four rows for it, and totals computed afterwards are silently inflated. Defend against it:

  • validate="many_to_one" (or "one_to_one", "one_to_many") raises if the keys don't have the expected uniqueness.
  • indicator=True adds a _merge column saying left_only, right_only or both, which is perfect for checking which rows failed to match.
  • Compare len() before and after a left join: it should not grow when the right side is a lookup table.

Key dtypes must be compatible: merging an int64 id with a string id raises an error, and float keys are fragile. Clean and convert keys first.

merge always returns a new DataFrame. For "keep rows of A whose key appears in B" without adding columns, A[A["key"].isin(B["key"])] is simpler than a join.

Example

import pandas as pd

orders = pd.DataFrame({"order_id": [1, 2, 3, 4], "cust_id": [10, 20, 10, 30], "amount": [50, 20, 70, 15]})
customers = pd.DataFrame({"id": [10, 20, 40], "name": ["Ana", "Bo", "Cy"]})

inner = orders.merge(customers, left_on="cust_id", right_on="id")
print(inner[["order_id", "name"]].values.tolist())   # [[1, 'Ana'], [2, 'Bo'], [3, 'Ana']]

left = orders.merge(customers, left_on="cust_id", right_on="id",
                    how="left", validate="many_to_one", indicator=True)
print(len(orders), len(left))                 # 4 4  no row explosion
print(left.loc[left["_merge"] == "left_only", "cust_id"].tolist())   # [30]

dup = pd.DataFrame({"id": [10, 10], "name": ["Ana", "Ana B."]})
try:
    orders.merge(dup, left_on="cust_id", right_on="id", validate="many_to_one")
except pd.errors.MergeError as e:
    print("caught:", type(e).__name__)      # duplicate keys in right

totals = orders.groupby("cust_id")["amount"].sum().rename("total")
print(customers.join(totals, on="id").fillna({"total": 0}))

Remember

  • merge(how=...) mirrors SQL inner/left/right/outer/cross joins
  • Use left_on/right_on for different key names and join for index joins
  • validate= and indicator=True catch duplicate keys and unmatched rows
  • Make key dtypes match before merging

Watch out for

  • A left join to a lookup table that has duplicate keys, silently multiplying rows and inflating totals.

Check yourself

  1. `left` has 100 rows; after `left.merge(lookup, on='k', how='left')` you get 130 rows. Most likely cause?
  2. What does `indicator=True` add to a merge result?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pandas.pydata.org/docs/user_guide/merging.html