merge(): Joining on Key Columns
Combine two tables by matching values in a key column — pd.merge / df.merge with on, left_on/right_on, and the default inner join (SQL JOIN for pandas) — video 10.
Watch the lesson
1 video on CodeWithMunnaX
One table has each batter's runs; another has each player's team. To answer "total runs per team" you need both facts in one row per player — matched by player name, not by row position. That lookup is a merge — pandas' version of a SQL JOIN (notebook 11: "merge() SQL join jaisa hota hai").
python
bat = pd.DataFrame({"player": ["Kohli", "Rohit", "Gill", "Pant"], "runs": [741, 417, 890, 446]})
info = pd.DataFrame({"player": ["Kohli", "Rohit", "Gill", "Bumrah"], "team": ["RCB", "MI", "GT", "MI"]})
print(bat)
print(info)text
player runs
0 Kohli 741
1 Rohit 417
2 Gill 890
3 Pant 446
player team
0 Kohli RCB
1 Rohit MI
2 Gill GT
3 Bumrah MIDefinition
merge()combines two DataFrames side by side by matching rows whose key column(s) have equal values, exactly like a database JOIN. Each output row is a pair of one left row and one right row that share the same key; thehowargument decides what happens to rows without a match.
- Key column — the column used to match rows (
player). It plays the role of an ID. - Left / right — the first DataFrame is the left table, the second is the right table. The order matters for
how="left"/"right". - Matching values — unlike
concat(axis=1), the row order does not matter: Gill in row 2 of one table matches Gill in row 0 of the other. - Default —
how="inner"(keep only keys present in both) andon=all columns with the same name in both tables.
Syntax
python
pd.merge(left, right, how="inner", on=None, left_on=None, right_on=None,
left_index=False, right_index=False, sort=False,
suffixes=("_x", "_y"), indicator=False, validate=None)
left.merge(right, ...) # same thing as a method| Parameter | Meaning | Default |
|---|---|---|
left, right | the two DataFrames (or a named Series as right) | required |
how | "inner", "left", "right", "outer", "cross" — which unmatched rows to keep (next topic) | "inner" |
on | key column name(s) present in both tables | common column names |
left_on / right_on | key column(s) when the names differ between tables | None |
left_index / right_index | use the row index as the key on that side | False |
sort | sort the result by the key | False |
suffixes | added to non-key columns that exist in both tables | ("_x", "_y") |
indicator | add a _merge column: left_only / right_only / both | False |
validate | check the key relationship: "one_to_one", "one_to_many", "many_to_one", "many_to_many" | None |
Example — the simplest merge
python
print(pd.merge(bat, info))
print(bat.merge(info, on="player"))text
player runs team
0 Kohli 741 RCB
1 Rohit 417 MI
2 Gill 890 GT
player runs team
0 Kohli 741 RCB
1 Rohit 417 MI
2 Gill 890 GTHow it works: with no on, pandas uses every column name the two tables share — here only player. It is an inner join, so only the three players in both tables survive: Pant (no team info) and Bumrah (no runs) are dropped. Always write on= explicitly — if the tables later gain another shared column (like season), the implicit merge silently changes.
text
bat info result (inner)
player runs player team player runs team
Kohli 741 <-------> Kohli RCB Kohli 741 RCB
Rohit 417 <-------> Rohit MI Rohit 417 MI
Gill 890 <-------> Gill GT Gill 890 GT
Pant 446 (no match) Bumrah MI (no match) -- both dropped --Example — keys with different names: left_on / right_on
python
squad = pd.DataFrame({"player": ["Kohli", "Rohit", "Gill"], "team_code": ["RCB", "MI", "GT"]})
teams = pd.DataFrame({"code": ["MI", "RCB", "GT"],
"team_name": ["Mumbai Indians", "Royal Challengers Bengaluru", "Gujarat Titans"]})
r = squad.merge(teams, left_on="team_code", right_on="code")
print(r)
print(r.drop(columns="code"))text
player team_code code team_name
0 Kohli RCB RCB Royal Challengers Bengaluru
1 Rohit MI MI Mumbai Indians
2 Gill GT GT Gujarat Titans
player team_code team_name
0 Kohli RCB Royal Challengers Bengaluru
1 Rohit MI Mumbai Indians
2 Gill GT Gujarat TitansHow it works: team_code (left) is matched with code (right). Both key columns are kept in the result because they have different names — drop the duplicate afterwards. Alternative: rename first, teams.rename(columns={"code": "team_code"}), then merge on="team_code".
Example — merging on several keys
python
a = pd.DataFrame({"season": [2023, 2023, 2024], "player": ["Kohli", "Gill", "Kohli"], "runs": [639, 890, 741]})
b = pd.DataFrame({"season": [2023, 2024, 2024], "player": ["Kohli", "Kohli", "Gill"], "sixes": [24, 38, 12]})
print(a.merge(b, on=["season", "player"], how="left"))text
season player runs sixes
0 2023 Kohli 639 24.0
1 2023 Gill 890 NaN
2 2024 Kohli 741 38.0How it works: with a list, a row matches only when all keys are equal — (2023, Kohli) matches (2023, Kohli) but not (2024, Kohli). Gill 2023 has no partner (only Gill 2024 exists), so sixes is NaN.
Example — merging on the index
python
ii = info.set_index("player")
print(bat.merge(ii, left_on="player", right_index=True, how="left"))text
player runs team
0 Kohli 741 RCB
1 Rohit 417 MI
2 Gill 890 GT
3 Pant 446 NaNright_index=True uses ii's index (player names) as its key. When both keys are indexes, left_index=True, right_index=True — or the shortcut join() (covered later).
Keys must really be equal
python
y2 = pd.DataFrame({"player": ["Kohli ", "rohit"], "team": ["RCB", "MI"]})
print(bat.merge(y2, on="player"))
y2["player"] = y2["player"].str.strip().str.title()
print(bat.merge(y2, on="player"))text
Empty DataFrame
Columns: [player, runs, team]
Index: []
player runs team
0 Kohli 741 RCB
1 Rohit 417 MI"Kohli " (trailing space) and "rohit" (lower case) do not equal "Kohli"/"Rohit" — the inner merge is empty. Clean keys with the string methods (.str.strip(), .str.title()) first. Different dtypes also fail: merging an int64 id with a str id raises ValueError: You are trying to merge on int64 and str columns for key 'id'... — convert with astype.
Common mistakes
- ❌ Relying on the implicit
on(all shared column names). ✅ Always passon=(orleft_on/right_on). - ❌ Expecting unmatched rows to stay with the default merge. ✅ Default is
how="inner"— usehow="left"to keep every left row. - ❌ Keys with extra spaces / different case / different dtypes → empty or partial result. ✅ Clean and convert keys before merging.
- ❌ Keeping both
team_codeandcodeafterleft_on/right_on. ✅ Drop one, or rename before merging.
Interview tip
Q: How is pd.merge different from pd.concat(axis=1)?
A: merge matches rows by the values of key columns (like SQL JOIN), so row order does not matter. concat(axis=1) just aligns rows by index label — it is glue, not a lookup.
Key points to remember
- merge() matches rows of two DataFrames by equal values in key column(s) — pandas' version of SQL JOIN.
- The default is how='inner' (only keys present in both) and on= every column name the tables share.
- Always pass on= explicitly; use left_on/right_on when the key columns have different names.
- A list of keys (on=['season', 'player']) matches only when all keys are equal.
- left_index=True / right_index=True use the row index as the key on that side.
- Keys must be exactly equal: strip spaces, fix case and align dtypes before merging.
- Row order does not matter in a merge — unlike concat(axis=1), which aligns by index label.
Try it yourself
Merge runs with team info
left_on / right_on and multiple keys
Practice: fix dirty keys, then merge
Revision Flashcards
Tap a card to flip it and see the answer.
