DataFrame Merging: merge vs. concat
After this topic, you will be able to:
- Clearly distinguish between
mergeandconcat. - Understand inner, left, right, and outer joins.
- Choose the correct merging method for practical scenarios.
When is merging necessary?
In practice, data is often distributed across multiple tables.
users table: orders table:
ββββββ¬βββββββ βββββββββββ¬βββββββββ¬βββββββββ
β id β name β β order_idβ user_idβ amount β
ββββββΌβββββββ€ βββββββββββΌβββββββββΌβββββββββ€
β 1 β Aliceβ β 101 β 1 β 5000 β
β 2 β Bob β β 102 β 2 β 3000 β
β 3 β Carolβ β 103 β 1 β 7000 β
ββββββ΄βββββββ βββββββββββ΄βββββββββ΄βββββββββTo calculate "total order amount per user," you need to combine these two tables. This is merging.
merge β Combining based on keys
merge() is equivalent to SQL's JOIN. It combines two DataFrames based on a common column (key).
import pandas as pd
users = pd.DataFrame({ "user_id": [1, 2, 3], "name": ["Alice", "Bob", "Carol"]})
orders = pd.DataFrame({ "order_id": [101, 102, 103], "user_id": [1, 2, 1], "amount": [5000, 3000, 7000]})
result = pd.merge(users, orders, on="user_id")print(result)# user_id name order_id amount# 0 1 Alice 101 5000# 1 1 Alice 103 7000# 2 2 Bob 102 3000Alice has two orders, so she appears in two rows. Carol does not have any orders, so she is not in the result β this is the default behavior of an inner join.
Types of Joins
users = pd.DataFrame({ "user_id": [1, 2, 3], "name": ["Alice", "Bob", "Carol"]})
orders = pd.DataFrame({ "order_id": [101, 102, 104], "user_id": [1, 2, 4], "amount": [5000, 3000, 8000]})# user_id=3 (Carol) has no orders, user_id=4 is not in the users table.Inner Join (default)
Only keys present in both sides.
pd.merge(users, orders, on="user_id", how="inner")# user_id name order_id amount# 0 1 Alice 101 5000# 1 2 Bob 102 3000Left Join
Keeps all rows from the left DataFrame. If a key is not in the right DataFrame, it fills the right DataFrame's columns with NaN.
pd.merge(users, orders, on="user_id", how="left")# user_id name order_id amount# 0 1 Alice 101.0 5000.0# 1 2 Bob 102.0 3000.0# 2 3 Carol NaN NaNCarol has no orders, but she is still included in the result.
Right Join
Keeps all rows from the right DataFrame.
pd.merge(users, orders, on="user_id", how="right")# user_id name order_id amount# 0 1 Alice 101 5000# 1 2 Bob 102 3000# 2 4 NaN 104 8000user_id=4 is not in the users table, but it is still included in the result.
Outer Join
Keeps all rows from both DataFrames.
pd.merge(users, orders, on="user_id", how="outer")# user_id name order_id amount# 0 1 Alice 101.0 5000.0# 1 2 Bob 102.0 3000.0# 2 3 Carol NaN NaN# 3 4 NaN 104.0 8000.0When key column names are different
users = pd.DataFrame({"id": [1, 2], "name": ["Alice", "Bob"]})orders = pd.DataFrame({"customer_id": [1, 2], "amount": [5000, 3000]})
# Use left_on / right_onpd.merge(users, orders, left_on="id", right_on="customer_id")# id name customer_id amount# 0 1 Alice 1 5000# 1 2 Bob 2 3000concat β Simply stacking
concat() combines DataFrames vertically or horizontally without key matching.
Vertical (axis=0, default)
jan = pd.DataFrame({"product": ["A", "B"], "sales": [100, 200]})feb = pd.DataFrame({"product": ["A", "B"], "sales": [150, 250]})
quarterly = pd.concat([jan, feb], ignore_index=True)# product sales# 0 A 100# 1 B 200# 2 A 150# 3 B 250When ignore_index=True, the index is reset to start from 0. If not used, the original index (0, 1, 0, 1) is duplicated.
Horizontal (axis=1)
info = pd.DataFrame({"name": ["Alice", "Bob"], "age": [30, 25]})scores = pd.DataFrame({"math": [90, 85], "english": [88, 92]})
combined = pd.concat([info, scores], axis=1)# name age math english# 0 Alice 30 90 88# 1 Bob 25 85 92merge vs. concat β When to use which
| merge | concat | |
|---|---|---|
| Purpose | Combine by matching keys | Simply stack/append |
| SQL Analogy | JOIN | UNION |
| Key Required | Yes (on, left_on/right_on) | No |
| Direction | Horizontal (add columns) | Vertical or horizontal |
| Use Case | Users + Orders, Products + Categories | Combine monthly data, combine split files |
"Connect users and orders" -> merge (key matching)
"Append January data and February data" -> concat (simple stacking)merge Caution β Duplication and Key Mismatch
Row explosion in many-to-many merges
# One user_id has multiple orders and shipmentsorders = pd.DataFrame({ "user_id": [1, 1, 1], "order_id": [101, 102, 103]})shipments = pd.DataFrame({ "user_id": [1, 1], "shipment_id": ["S1", "S2"]})
result = pd.merge(orders, shipments, on="user_id")print(len(result)) # 6! (3 Γ 2 = Cartesian product)When there are multiple keys on both sides, all combinations are generated. 3 rows Γ 2 rows = 6 rows. This type of merging can cause memory to explode in large datasets. You can prevent this with the validate parameter:
pd.merge(orders, shipments, on="user_id", validate="many_to_one")# MergeError: Merge keys are not unique in right datasetPractical Pattern β Combining Multiple Files
import osimport pandas as pd
data_dir = "monthly_reports"all_dfs = []
for filename in sorted(os.listdir(data_dir)): if filename.endswith(".csv"): filepath = os.path.join(data_dir, filename) df = pd.read_csv(filepath) df["source_file"] = filename all_dfs.append(df)
combined = pd.concat(all_dfs, ignore_index=True)print(f"Total rows: {len(combined)}")This is a pattern for combining monthly CSV files into one. You can add a source_file column to track which file it came from.
Key Takeaways
| Function | Purpose | Key Parameters |
|---|---|---|
merge() | Key-based merging (JOIN) | on, how, left_on/right_on |
concat() | Simple stacking (UNION) | axis, ignore_index |
| Join | Scope |
|---|---|
| inner | Keys present in both sides |
| left | All from left + matching from right |
| right | All from right + matching from left |
| outer | All from both (NaN if missing) |
80% of data analysis is about correctly combining the right tables in the right way. Understanding the difference between merge and concat, and the behavior of the four types of joins, can help you prevent common data preprocessing mistakes (duplicate rows, NaN explosion, key mismatches).