Reshaping β Pivot, Melt, Stack
Definition
Reshaping converts data between wide format (one row per entity, variables as columns) and long/tidy format (one row per observation). pandasβ main tools:
pivot,pivot_table,melt,stack/unstack, andcrosstab.
graph LR W["Wide format (entity x variable columns)"] -- melt --> L["Long format (id, variable, value)"] L -- pivot --> W
melt() β Wide β Long
df.melt(
id_vars=None, # columns to keep fixed (identifiers)
value_vars=None, # columns to unpivot; default = all others
var_name="variable", # name for the new "variable" column
value_name="value" # name for the new "value" column
)df_wide = pd.DataFrame({
"student": ["A", "B"],
"math": [90, 80],
"science": [85, 95]
})
df_wide.melt(id_vars="student", var_name="subject", value_name="score")
# student subject score
# 0 A math 90
# 1 B math 80
# 2 A science 85
# 3 B science 95pivot() β Long β Wide (no aggregation, requires unique index/column pairs)
df.pivot(index="student", columns="subject", values="score")
pivot()fails on duplicate (index, columns) combinationsIf there are multiple rows for the same
(index, columns)pair,pivot()raises aValueError. Usepivot_table()instead if aggregation is needed.
pivot_table() β Long β Wide, WITH Aggregation
pd.pivot_table(
df,
values="score", # column to aggregate
index="student", # row grouper
columns="subject", # column grouper
aggfunc="mean", # 'sum' | 'mean' | 'count' | list | dict | custom func
fill_value=0, # replace resulting NaN
margins=True, # add row/column totals ("All")
margins_name="Total"
)pd.pivot_table(
sales_df,
values="revenue",
index="region",
columns="quarter",
aggfunc="sum",
fill_value=0,
margins=True
)
pivot_tableβ Excel PivotTableConceptually identical to building an Excel Pivot Table: rows =
index, columns =columns, cell values =aggfunc(values).
stack() / unstack() β Move Between Columns and (Multi)Index
df.stack() # pivot innermost COLUMN level into the row index (wide -> long)
df.unstack() # pivot innermost ROW index level into columns (long -> wide)
df.unstack(level=0) # specify which index level to unstack
df.stack(dropna=False) # keep resulting NaN rows instead of dropping# Typical use after groupby with multiple keys
s = df.groupby(["region", "quarter"])["revenue"].sum()
s.unstack() # region as rows, quarter as columnscrosstab() β Frequency / Contingency Tables
pd.crosstab(df["gender"], df["city"]) # counts
pd.crosstab(df["gender"], df["city"], normalize="index") # row percentages
pd.crosstab(df["gender"], df["city"], values=df["age"], aggfunc="mean") # custom aggregation
pd.crosstab([df["gender"], df["age_group"]], df["city"]) # multi-level rows
crosstabvspivot_table
crosstabdefaults to counting occurrences (like a frequency table);pivot_tabledefaults to mean and expects you to specifyvalues. Usecrosstabfor quick βhow many of each combinationβ questions.
wide_to_long() β Structured Column Prefix Reshaping
pd.wide_to_long(
df,
stubnames="score", # common prefix, e.g. score1, score2 -> "score"
i="student", # id column
j="attempt" # name for suffix-derived column
)explode() β Expand List-Like Cells into Rows
df = pd.DataFrame({"id": [1, 2], "tags": [["a", "b"], ["c"]]})
df.explode("tags")
# id tags
# 0 1 a
# 0 1 b
# 1 2 cT β Transpose
df.T # swap rows and columns