The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Keep matching keysresult = orders.merge(customers, on='customer_id', how='inner')View examples
Preserve left rowsresult = orders.merge(customers, on='customer_id', how='left')View examples
Join different key namesresult = orders.merge(customers, left_on='customer_id', right_on='id', how='left')View examples
Validate key cardinalityresult = orders.merge(customers, on='customer_id', validate='many_to_one')View examples
Track row provenanceaudit = left.merge(right, on='id', how='outer', indicator=True)View examples
Label overlapping columnsresult = current.merge(previous, on='id', suffixes=('_current', '_previous'))View examples
Stack row batchescombined = pd.concat([january, february], ignore_index=True)View examples
Require identical columnscombined = pd.concat(frames, join='inner', ignore_index=True)View examples
Align columns by indexcombined = pd.concat([features, targets], axis='columns')View examples
Preserve batch identitycombined = pd.concat({'jan': january, 'feb': february}, names=['month'])View examples
Join on indexesresult = customers.join(accounts, how='left', validate='one_to_one')View examples
Fill from a fallbackcombined = primary.combine_first(fallback)View examples
Match the nearest prior keyresult = pd.merge_asof(events, rates, on='time', direction='backward')View examples
Show changed valueschanges = before.compare(after, result_names=('before', 'after'))View examples

Combining tables is safest when key cardinality is declared and checked. Use merge for relational keys, join for index-oriented alignment, concat for stacking along an axis, and diagnostic indicators or comparisons to expose records that did not combine as expected.

Step by step

Detailed examples

01

Choose join keys and preservation rules

inner merge keeps matches; left merge preserves the left table and represents unmatched right values as missing. Name keys explicitly rather than relying on every shared column. Repeated keys on both sides create a Cartesian product within each key, and pandas matches null keys to one another unlike typical SQL joins, so validate or remove missing keys when that behavior is not intended.

Attach customer names to orders
import pandas as pd

orders = pd.DataFrame({'order_id': [1, 2, 3], 'customer_id': [10, 20, 30]})
customers = pd.DataFrame({'id': [10, 20], 'name': ['Ada', 'Lin']})

inner = orders.merge(customers, left_on='customer_id', right_on='id', how='inner')
left = orders.merge(customers, left_on='customer_id', right_on='id', how='left')

print(inner[['order_id', 'name']].to_dict('records'))
print(left[['order_id', 'name']].to_dict('records'))
Output
[{'order_id': 1, 'name': 'Ada'}, {'order_id': 2, 'name': 'Lin'}]
[{'order_id': 1, 'name': 'Ada'}, {'order_id': 2, 'name': 'Lin'}, {'order_id': 3, 'name': nan}]
Back to quick reference ↑
02

Assert cardinality and inspect unmatched keys

validate turns an assumed key relationship into an executable check before the merge result is accepted. Use many_to_one for fact-to-dimension enrichment, one_to_one for unique records, and the other declared relationships only when duplicates are intended. An outer merge with indicator exposes left-only and right-only keys, while explicit suffixes keep overlapping measurements understandable.

Validate enrichment and audit provenance
import pandas as pd

orders = pd.DataFrame({'customer_id': [10, 10, 20], 'status': ['new', 'paid', 'new']})
customers = pd.DataFrame({'customer_id': [10, 20, 30], 'status': ['active', 'active', 'paused']})

enriched = orders.merge(
    customers, on='customer_id', how='left', validate='many_to_one', suffixes=('_order', '_customer'),
)
audit = orders[['customer_id']].drop_duplicates().merge(
    customers[['customer_id']], on='customer_id', how='outer', indicator=True, validate='one_to_one',
)

print(enriched.columns.tolist())
print(audit.set_index('customer_id')['_merge'].astype(str).to_dict())
Output
['customer_id', 'status_order', 'status_customer']
{10: 'both', 20: 'both', 30: 'right_only'}
Back to quick reference ↑
03

Concatenate compatible batches or aligned features

concat stacks objects without relational key matching. Along rows, the default outer column union can introduce missing values when schemas differ; use join='inner' only when dropping non-shared columns is intentional. Along columns, pandas aligns index labels rather than row positions. Mapping keys create a source level that retains batch provenance.

Stack monthly batches and align model columns
import pandas as pd

january = pd.DataFrame({'id': [1, 2], 'amount': [10, 20]})
february = pd.DataFrame({'id': [3], 'amount': [30], 'note': ['late']})

rows = pd.concat([january, february], ignore_index=True)
strict_rows = pd.concat([january, february], join='inner', ignore_index=True)
keyed = pd.concat({'jan': january, 'feb': february}, names=['month'])

features = pd.DataFrame({'score': [0.8, 0.6]}, index=[101, 102])
targets = pd.DataFrame({'approved': [True, False]}, index=[102, 103])
columns = pd.concat([features, targets], axis='columns')

print(rows.columns.tolist())
print(strict_rows.to_dict('records'))
print(keyed.index.names)
print(columns.index.tolist())
Output
['id', 'amount', 'note']
[{'id': 1, 'amount': 10}, {'id': 2, 'amount': 20}, {'id': 3, 'amount': 30}]
['month', None]
[101, 102, 103]
Back to quick reference ↑
04

Use index alignment for labeled observations

DataFrame.join is convenient when row indexes are already meaningful keys and can validate their relationship. combine_first takes non-missing values from the caller and fills remaining locations from another aligned object, using the union of labels. It is a coalescing operation, not a conflict detector; compare present values separately when disagreements matter.

Join account tiers and fill profile gaps
import pandas as pd

customers = pd.DataFrame({'name': ['Ada', 'Lin']}, index=pd.Index([10, 20], name='customer_id'))
accounts = pd.DataFrame({'tier': ['pro', 'basic']}, index=pd.Index([10, 20], name='customer_id'))
joined = customers.join(accounts, how='left', validate='one_to_one')

primary = pd.Series({10: 'ada@example.com', 20: None}, name='email')
fallback = pd.Series({20: 'lin@example.com', 30: 'sam@example.com'}, name='email')
combined = primary.combine_first(fallback)

print(joined.to_dict('index'))
print(combined.to_dict())
Output
{10: {'name': 'Ada', 'tier': 'pro'}, 20: {'name': 'Lin', 'tier': 'basic'}}
{10: 'ada@example.com', 20: 'lin@example.com', 30: 'sam@example.com'}
Back to quick reference ↑
05

Match ordered observations and compare revisions

merge_asof performs nearest-key matching for sorted numeric or datetime keys; backward matching is useful for attaching the latest state known at an event time. Add by for independent entities and tolerance to reject stale matches. DataFrame.compare requires identical labels and shows value differences, making it suitable after both versions have been deliberately aligned.

Attach prior rates and report changed records
import pandas as pd

events = pd.DataFrame({
    'time': pd.to_datetime(['2026-08-01 09:05', '2026-08-01 09:40']),
    'amount': [100, 120],
}).sort_values('time')
rates = pd.DataFrame({
    'time': pd.to_datetime(['2026-08-01 09:00', '2026-08-01 09:30']),
    'rate': [1.1, 1.2],
}).sort_values('time')
matched = pd.merge_asof(events, rates, on='time', direction='backward')

before = pd.DataFrame({'status': ['open', 'paid'], 'amount': [100, 120]}, index=[1, 2])
after = pd.DataFrame({'status': ['paid', 'paid'], 'amount': [100, 125]}, index=[1, 2])
changes = before.compare(after, result_names=('before', 'after'))

print(matched['rate'].tolist())
print(changes.columns.tolist())
Output
[1.1, 1.2]
[('status', 'before'), ('status', 'after'), ('amount', 'before'), ('amount', 'after')]
Back to quick reference ↑

Local code tester

Validate and audit a table merge

Change the customer keys and inspect matched, left-only, and right-only records.

Runs in your browser
Output
Press Run to load Python locally.

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. pandas development teamMerge, join, concatenate and comparepandas.pydata.org
  2. pandas development teampandas.mergepandas.pydata.org
  3. pandas development teampandas.concatpandas.pydata.org
  4. pandas development teampandas.merge_asofpandas.pydata.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback