Handbooks / Pandas & Data Analysis / Chapter 4

Merging and Reshaping

7 pages · ~5 min✓ Reviewed

Builds on GroupBy and Aggregation. Next up: Time Series.

Pandas & Data AnalysisPage 1

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

howKeepsOther side
innerkeys in both framesno NaN
leftevery left rowright columns NaN
rightevery right row (mirror of left)left columns NaN
outerall keys from bothNaN on the missing side
python
orders.merge(customers, on='cust_id', how='left')
order_idcust_idname
1c1Ana
2c2Ben
3c9NaN

all orders kept, even when the customer is missing

3. Duplicate keys multiply

  1. 2 left rowskey A
  2. 3 right rowskey A
  3. 6 rows2 x 3
  • Find unmatched rows: how='left', indicator=True, then _merge == 'left_only'
Silent growth

Repeated keys on both sides multiply rows. Compare the row count before and after every merge.

Pandas & Data AnalysisPage 2

Join Keys, Suffixes and Validation

1. Keys and defaults

Key columnsArgument
Same nameon='k'
Different namesleft_on='a_id', right_on='b_id' (both columns stay)
Indexleft_index=True, right_index=True
  • df.join(other): joins on the index, default left
  • pd.merge: joins on shared column names, default inner

2. Suffixes

  • Overlapping non-key columns get _x, _y by 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 fails
  • indicator=True adds _merge: left_only, right_only, both
python
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']

output
   a_id  b_id     _merge
0     1   1.0       both
1     2   2.0       both
2     3   NaN  left_only
Audit needs how='left'

Default inner keeps only matches, so left_only never appears. Use how='left' or 'outer'.

Pandas & Data AnalysisPage 3

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
python
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

output
     x    y
a 0  1  NaN
  1  2  NaN
b 0  3  9.0

2. Axis and Join

axis=0axis=1
Directionstack rowsside by side
Aligns oncolumn namesindex labels
join='inner'shared onlyshared 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
Pandas & Data AnalysisPage 4

Melt and Pivot: Wide vs Long

1. Wide to Long: melt

  • pd.melt turns the listed value_vars columns into rows
  • id_vars stay as columns; names go to var_name, cells to value_name
python
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
pivotpivot_table
Duplicate pairsValueErroraggregated
aggfuncnonedefault mean
fill_valuenofill_value=0
Use onclean, unique datareal data

3. Round Trip

  1. meltwide to long
  2. groupbyaggregate
  3. pivot_tableback to wide
Real data has duplicates

Prefer pivot_table(..., aggfunc='sum', fill_value=0); plain pivot fails on repeated pairs.

Pandas & Data AnalysisPage 5

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)
pivotunstack
Works onDataFrame columnsan index level
Called aspivot(index, columns, values)unstack(level)

2. MultiIndex Basics

  • MultiIndex = index with several levels
  • Build: set_index(['a','b']) or groupby(['a','b'])
  • Read: loc[('a1','b1')] for one row, xs('b1', level='b') for one level
python
s = df.set_index(['a','b'])['v']
s.unstack()
s.unstack(fill_value=0)
s.loc[('a1','b1')]
s.xs('b1', level='b')
ab1b2
a11.02.0
a23.0NaN

df has columns a, b, v

3. Gotchas

  • unstack() creates NaN for missing combinations; fill_value=0 fills them
  • pandas 3: stack() keeps NaN; older versions dropped all-NaN rows
  • Slicing a MultiIndex range needs sort_index() first
Flatten

reset_index() turns every MultiIndex level back into an ordinary column.

Pandas & Data AnalysisPage 6

Cheat Sheet: Merge, Concat and Reshape

1. Join by key

  • how= inner | left | right | outer
  • validate='1:1' raises on bad keys; indicator=True adds _merge
  • Key names differ: left_on= + right_on=; df.join(o) joins on the index
python
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

JobCall
Glue framespd.concat([a,b], axis=0|1, ignore_index=True, join='outer')
Wide to longpd.melt(df, id_vars, value_vars, var_name, value_name)
Long to widepivot (unique pairs) or pivot_table (aggfunc, fill_value)
Columns into indexdf.stack(); back with s.unstack(level)
Column or indexset_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, _merge counts, validate=
Row explosion

Duplicate keys multiply rows, and merge defaults to inner. Compare row counts before and after.

Pandas & Data AnalysisPage 7

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=True to get a clean 0..3 index