The Split-Apply-Combine paradigm is encoded in df.groupby('key'). After grouping, aggregations like df.groupby('key').mean(numeric_only=True) or .sum() collapse each group into a single row; grouping by multiple columns, df.groupby(['k1', 'k2']).sum(), produces a MultiIndex on the result. To iterate over the groups, write for name, group in df.groupby('key'): .... When several aggregations are needed per group, df.groupby('key').agg({'a': 'mean', 'b': ['min', 'max']}) returns all of them in one call, and named aggregation df.groupby('key').agg(mean_a=('a', 'mean'), max_b=('b', 'max')) assigns clean column names to each result.
Two advanced groupby methods preserve the shape of the original frame. df.groupby('key')['v'].transform('mean') broadcasts the per-group mean back to every row, aligning the result with the input index — useful for adding group-normalized columns. df.groupby('key').filter(lambda g: g['v'].mean() > 0) instead drops entire groups that fail a boolean condition. Combining frames is done either by stacking or joining. pd.concat([df1, df2]) stacks rows vertically, with ignore_index=True renumbering rows from zero; passing axis=1 instead glues frames side-by-side as new columns. For relational joins, pd.merge(df1, df2, on='key') works like a SQL join; the how parameter takes 'left', 'right', 'inner' (the default), or 'outer'. When the join keys have different names, use left_on='id1', right_on='id2'. df1.join(df2) is a convenience that defaults to merging on the index, while merge is the more general form.
Reshaping rotates the layout of a frame. df.pivot(index='id', columns='var', values='val') goes from long to wide but requires unique index/column pairs; when duplicates exist, df.pivot_table(index='id', columns='var', values='val', aggfunc='sum') aggregates them (defaulting to mean). The reverse, df.melt(id_vars=['id'], var_name='var', value_name='val'), turns wide columns into long rows. stack() compresses a level of the column index into the row index, yielding a Series with a MultiIndex, and unstack() does the opposite. Cross-tabulations are produced with pd.crosstab(df['a'], df['b']), with optional values= and aggfunc= to summarize another column. Finally, continuous values can be binned into discrete buckets with pd.cut(df['v'], bins=[0, 10, 20, 100]), which returns a categorical Series, or with pd.qcut(df['v'], q=4) for equal-frequency quartiles.