Handbooks / Pandas & Data Analysis / Chapter 4
Merging and Reshaping
7 pages · ~5 min✓ Reviewed
Builds on GroupBy and Aggregation. Next up: Time Series.
Merge Mental Model and Join Types
1. What merge does
pd.merge(left, right, on='key', how='inner')works like a SQL JOIN- Matches rows by key values, not by position or index label
2. Join types
| how | Keeps | Other side |
|---|---|---|
inner | keys in both frames | no NaN |
left | every left row | right columns NaN |
right | every right row (mirror of left) | left columns NaN |
outer | all keys from both | NaN on the missing side |
orders.merge(customers, on='cust_id', how='left')
| order_id | cust_id | name |
|---|---|---|
| 1 | c1 | Ana |
| 2 | c2 | Ben |
| 3 | c9 | NaN |
all orders kept, even when the customer is missing
3. Duplicate keys multiply
- 2 left rowskey A
- 3 right rowskey A
- 6 rows2 x 3
- Find unmatched rows:
how='left', indicator=True, then_merge == 'left_only'
Repeated keys on both sides multiply rows. Compare the row count before and after every merge.
Join Keys, Suffixes and Validation
1. Keys and defaults
| Key columns | Argument |
|---|---|
| Same name | on='k' |
| Different names | left_on='a_id', right_on='b_id' (both columns stay) |
| Index | left_index=True, right_index=True |
df.join(other): joins on the index, default leftpd.merge: joins on shared column names, default inner
2. Suffixes
- Overlapping non-key columns get
_x,_yby default - Pass
suffixes=('_l','_r')for readable names
3. Validate and audit
validate='one_to_one','one_to_many','many_to_one'raises MergeError if uniqueness failsindicator=Trueadds_merge:left_only,right_only,both
a = pd.DataFrame({'a_id': [1, 2, 3]})
b = pd.DataFrame({'b_id': [1, 2]})
r = a.merge(b, left_on='a_id', right_on='b_id',
how='left', validate='one_to_one', indicator=True)
print(r)find unmatched rows: r[r['_merge'] == 'left_only']
a_id b_id _merge 0 1 1.0 both 1 2 2.0 both 2 3 NaN left_only
Default inner keeps only matches, so left_only never appears. Use how='left' or 'outer'.
Concat: Gluing Frames Together
1. Stacking Rows
pd.concat([a, b])stacks rows (axis=0)- Columns align by name
- Column in one frame only → NaN in the other rows
import pandas as pd a = pd.DataFrame({'x': [1, 2]}) b = pd.DataFrame({'x': [3], 'y': [9]}) print(pd.concat([a, b], keys=['a', 'b']))
keys= adds a source level to the index
x y a 0 1 NaN 1 2 NaN b 0 3 9.0
2. Axis and Join
| axis=0 | axis=1 | |
|---|---|---|
| Direction | stack rows | side by side |
| Aligns on | column names | index labels |
join='inner' | shared only | shared only |
join='outer' | all, NaN gaps (default) | all, NaN gaps (default) |
3. Index, Keys, Interview
ignore_index=True→ clean 0..n-1 index- Without it indexes repeat:
loc[0]returns several rows keys=['a','b']→ MultiIndex of source + old index
- concat or merge? concat stacks or aligns by label; merge matches key values
- Repeated index? Each frame keeps its own labels
- One-frame column? Kept; other rows get NaN
Melt and Pivot: Wide vs Long
1. Wide to Long: melt
pd.meltturns the listed value_vars columns into rows- id_vars stay as columns; names go to
var_name, cells tovalue_name
long = pd.melt(df, id_vars=['id'], value_vars=['q1', 'q2'], var_name='quarter', value_name='sales')
one row per id and quarter
2. Long to Wide: pivot vs pivot_table
df.pivot(index='id', columns='quarter', values='sales')makes long into wide- Reverse a melt with pivot, when pairs are unique
| pivot | pivot_table | |
|---|---|---|
| Duplicate pairs | ValueError | aggregated |
| aggfunc | none | default mean |
| fill_value | no | fill_value=0 |
| Use on | clean, unique data | real data |
3. Round Trip
- meltwide to long
- groupbyaggregate
- pivot_tableback to wide
Prefer pivot_table(..., aggfunc='sum', fill_value=0); plain pivot fails on repeated pairs.
Stack, Unstack and MultiIndex Basics
1. Stack and Unstack
df.stack(): innermost column level → row index (wide to long)s.unstack(level=-1): index level → columns (long to wide)
| pivot | unstack | |
|---|---|---|
| Works on | DataFrame columns | an index level |
| Called as | pivot(index, columns, values) | unstack(level) |
2. MultiIndex Basics
- MultiIndex = index with several levels
- Build:
set_index(['a','b'])orgroupby(['a','b']) - Read:
loc[('a1','b1')]for one row,xs('b1', level='b')for one level
s = df.set_index(['a','b'])['v'] s.unstack() s.unstack(fill_value=0) s.loc[('a1','b1')] s.xs('b1', level='b')
| a | b1 | b2 |
|---|---|---|
| a1 | 1.0 | 2.0 |
| a2 | 3.0 | NaN |
df has columns a, b, v
3. Gotchas
unstack()creates NaN for missing combinations;fill_value=0fills them- pandas 3:
stack()keeps NaN; older versions dropped all-NaN rows - Slicing a MultiIndex range needs sort_index() first
reset_index() turns every MultiIndex level back into an ordinary column.
Cheat Sheet: Merge, Concat and Reshape
1. Join by key
how=inner | left | right | outervalidate='1:1'raises on bad keys;indicator=Trueadds_merge- Key names differ:
left_on=+right_on=;df.join(o)joins on the index
r = a.merge(b, on='k', how='left', suffixes=('_l','_r'), validate='1:1', indicator=True) r[r['_merge'] == 'left_only'] a.merge(b, left_on='a_id', right_on='b_id')
how='left' keeps unmatched rows so left_only can find them
2. Combine and reshape
| Job | Call |
|---|---|
| Glue frames | pd.concat([a,b], axis=0|1, ignore_index=True, join='outer') |
| Wide to long | pd.melt(df, id_vars, value_vars, var_name, value_name) |
| Long to wide | pivot (unique pairs) or pivot_table (aggfunc, fill_value) |
| Columns into index | df.stack(); back with s.unstack(level) |
| Column or index | set_index / reset_index |
3. Interview Point
- Which tool? merge or join by key, concat to glue, pivot_table to aggregate, unstack to move a level
- Checks after a merge? row count,
_mergecounts,validate=
Duplicate keys multiply rows, and merge defaults to inner. Compare row counts before and after.
Check yourself
left keys are [A, A, B], right keys are [A, A, A]. How many rows does a how='left' merge on the key return?
- 7: the two left A rows each match three right rows, giving 6
- B has no match, so it stays as 1 row with NaN on the right columns
df.pivot(index='id', columns='quarter', values='sales') raises ValueError. What is the likely cause and fix?
- An id/quarter pair appears twice, so a cell has two values
- Use
pivot_table(..., aggfunc='sum')to aggregate them
Two frames, each with index 0 and 1, go through pd.concat([a, b]). What does df.loc[0] return?
- Two rows: the index repeats, so label 0 exists twice
- Pass
ignore_index=Trueto get a clean 0..3 index