Grouping, joining and reshaping
split-apply-combine with groupby, merging tables like SQL, and moving between long and wide layouts.
groupby: split, apply, combine
df.groupby("team")["score"].mean()
df.groupby(["team", "level"])["score"].agg(["count", "mean", "max"])
summary = df.groupby("team").agg(
n=("score", "size"),
avg=("score", "mean"),
best=("name", lambda s: s.value_counts().index[0]),
).reset_index()
df.groupby("team")["score"].transform("mean") # same length as df| Method | Shape of result |
|---|---|
agg | One row per group |
transform | Same number of rows — adds a column |
filter | Keeps whole groups that pass a test |
apply | Arbitrary function per group (slowest) |
💡
transform is underused and often the cleanest answer: "add each row's group average as a new column" is a transform, not a re-merge.Merging like SQL
orders.merge(customers, on="customer_id", how="left")
orders.merge(customers, left_on="cust", right_on="id", how="inner")
pd.concat([jan, feb], ignore_index=True) # stack rows
pd.concat([a, b], axis=1) # side by side, aligned on indexhow:inner,left,right,outer— mirroring SQL joins.- Check
validate="one_to_many"to have pandas fail loudly on an unexpected relationship. - After a left join, count nulls in the right-hand key to see how many rows failed to match.
⚠️
A join on non-unique keys silently multiplies rows — the same fan-out trap as in SQL. Compare
len(df) before and after, and pass validate= so the relationship is asserted rather than assumed.Long and wide
# wide -> long
long = df.melt(id_vars=["name"], value_vars=["math", "science"],
var_name="subject", value_name="score")
# long -> wide
wide = long.pivot(index="name", columns="subject", values="score")
wide.reset_index(inplace=True)
# safe aggregation when keys repeat
long.pivot_table(index="name", columns="subject", values="score", aggfunc="mean")Plotting libraries and most statistical models expect long data; humans read wide tables more easily. melt and pivot are the two operations between those worlds.
FAQ
Why did my merge produce more rows than I started with?
The join key is not unique on the right-hand side, so each match duplicates the left row. Verify with
df["key"].is_unique or pass validate=.How do I avoid the index after groupby?
Add
.reset_index(), or pass as_index=False to the groupby call when you want a normal column back.Related
Selecting, filtering and cleaning Joins
Last refreshed 2026-09-18.