Joins & Merges
Combining tables on shared keys is where silent row duplication and lost records happen. pandas merge and concat need explicit join types and validation.
Recipe
Quick-reference recipe card - copy-paste ready.
import pandas as pd
merged = left.merge(
right,
on="order_id",
how="left",
validate="many_to_one",
indicator=True,
)
unmatched = merged.loc[merged["_merge"] == "left_only"]When to reach for this:
- Enriching fact tables with dimension attributes
- Stitching API responses from multiple endpoints
- Unioning monthly Parquet partitions vertically
- Building training sets from features + labels tables
Working Example
import pandas as pd
orders = pd.DataFrame(
{"order_id": [1, 2, 3], "customer_id": [10, 11, 10], "amount": [120, 340, 80]}
)
customers = pd.DataFrame(
{"customer_id": [10, 11], "segment": ["SMB", "Enterprise"]}
)
refunds = pd.DataFrame({"order_id": [2], "refund": [50.0]})
# Dimension enrich - expect 1:1 or many:1
enriched = orders.merge(
customers,
on="customer_id",
how="left",
validate="many_to_one",
)
# Fact extension - refunds may be missing
with_refunds = enriched.merge(
refunds,
on="order_id",
how="left",
validate="one_to_one",
)
with_refunds["refund"] = with_refunds["refund"].fillna(0.0)
with_refunds["net"] = with_refunds["amount"] - with_refunds["refund"]
# Audit join coverage
audit = orders.merge(customers, on="customer_id", how="left", indicator=True)
missing_customers = audit.loc[audit["_merge"] == "left_only"]
print(with_refunds)
print("missing customer rows:", len(missing_customers))What this demonstrates:
validateto assert cardinality expectations- Left join preserving all orders
indicator=Truefor orphan detection- Fill NA after left join for optional facts
Deep Dive
How It Works
mergeperforms database-style joins on column keys (hash or sort-merge).howcontrols which keys survive: inner, left, right, outer.validateraises if actual cardinality violates the declared relationship.concatstacks along axis 0 (rows) or 1 (columns) without key alignment.
Join Types
| how | Keeps |
|---|---|
| inner | Keys in both |
| left | All left keys |
| right | All right keys |
| outer | Union of keys |
Python Notes
import pandas as pd
# Different column names
pd.merge(orders, regions, left_on="region_id", right_on="id")
# Union monthly files with consistent columns
pd.concat([jan, feb], ignore_index=True)Gotchas
- Many-to-many merge - duplicates explode row count multiplicatively. Fix: dedupe keys first or use
validate="m:m"knowingly with row count checks. - Key dtype mismatch -
int64vsobject"1" yields empty join. Fix:astypekeys on both sides before merge. - Duplicate column names - suffixes default to
_x/_y. Fix:suffixes=("", "_dim")and drop redundant cols. - Merge on index accidentally - forgotten
on=joins on index if aligned. Fix:reset_index()or explicitleft_indexflags. - concat without ignore_index - preserves duplicate row labels. Fix:
ignore_index=Trueor hierarchical index by source.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
DuckDB read_parquet + SQL | Complex multi-table joins | Two small in-memory frames |
Polars join | Large lazy joins | Already deep in pandas pipeline |
DataFrame.join | Index-aligned wide tables | Key columns not in index |
| Database ETL | Data already lives in warehouse | Notebook-only exploration |
FAQs
How do I detect a many-to-many explosion?
assert len(merged) == len(left) # for many-to-one- Compare
lenbefore/after; investigate if product grows.
When use concat vs merge?
concat: same schema, stack periods or shards.merge: different tables sharing keys.
How do I merge on multiple keys?
left.merge(right, on=["region", "month"])What does indicator=True do?
- Adds
_mergecolumn:left_only,right_only,both. - Quick data quality signal after left joins.
How do I anti-join (rows in left not in right)?
left.merge(right, on="id", how="left", indicator=True).query("_merge == 'left_only'")Should I set indexes before join?
- Not required -
on=is clearer for most analytics. - Index joins help repeated joins on same key in tight loops.
How do I join time series asof?
pd.merge_asof(trades.sort_values("ts"), quotes.sort_values("ts"), on="ts")- Picks nearest prior quote per trade timestamp.
Can I merge with different granularities?
- Aggregate to common grain first (daily vs hourly).
- Many-to-many often means grain mismatch.
How do I drop duplicate key rows?
right = right.drop_duplicates("customer_id", keep="last")Does merge preserve order?
- Not guaranteed - sort output explicitly for presentations.
Related
- Cleaning & Transforming Data - normalize keys first
- GroupBy & Aggregation - aggregate after enrich
- File Formats - Parquet partitions to concat
- Data Analysis Best Practices - reproducible joins
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.