pivot_table in pandas: The Excel Pivot, Written Down
Everybody who has used Excel knows what a pivot table does. pivot_table is the same operation with the choices written down instead of dragged, which means it re-runs next month without anybody remembering where the fields went.
What you do: name four things. Rows, columns, the value, and how to aggregate it. The fourth one has a default you almost never want.
The short version. aggfunc defaults to mean. Pass it explicitly, every time.
The table
import pandas as pd
df = pd.DataFrame({
'order_id': [101, 102, 103, 104, 105, 106, 107, 108],
'region': ['North','North','South','South','East','West','West','East'],
'month': ['Jan','Feb','Jan','Feb','Jan','Jan','Feb','Feb'],
'amount': [120, 80, 200, 50, 300, 90, 60, 150],
'status': ['shipped','shipped','cancelled','shipped',
'shipped','pending','shipped','cancelled'],
})
The four arguments
df.pivot_table(index='region', columns='month', values='amount',
aggfunc='sum', fill_value=0, margins=True, margins_name='Total')
| region | Feb | Jan | Total |
|---|---|---|---|
| East | 150 | 300 | 450 |
| North | 80 | 120 | 200 |
| South | 50 | 200 | 250 |
| West | 60 | 90 | 150 |
| Total | 340 | 710 | 1050 |
Four arguments do the work. index is the rows, columns is the columns, values is the number, aggfunc is what to do with it. fill_value replaces the NaN in combinations that never occurred, and margins adds the totals.
Note the column order: Feb before Jan, because pandas sorts the column values alphabetically and has no idea they are months. Fixing that is a separate step, below.
aggfunc out and you get the mean. On this table that turns East from 450 into 225, and the result is a perfectly formatted pivot of numbers that are not what you asked for. Nothing errors, because a mean is a legitimate thing to want.The fill value is a decision
A missing combination is not the same as a zero. If West genuinely had no February orders, fill_value=0 is honest. If February data has not loaded yet, a zero is a fabricated observation that will be charted as a real month with no sales.
When you are not sure, leave the NaN in. It is visible, it propagates through arithmetic, and it forces the question. That is the same argument as in fillna and dropna.
Several values, several aggregations
df.pivot_table(index='region', values=['amount', 'order_id'],
aggfunc={'amount': 'sum', 'order_id': 'count'})
A dictionary lets each column have its own function, which is how you get a total and a count side by side. Passing a list such as aggfunc=['sum','mean'] applies every function to every value column and produces a multi-level column index, which is powerful and hard to read. Prefer the dictionary.
Fixing the column order
months = ['Jan','Feb','Mar','Apr','May','Jun',
'Jul','Aug','Sep','Oct','Nov','Dec']
df['month'] = pd.Categorical(df['month'], categories=months, ordered=True)
df.pivot_table(index='region', columns='month', values='amount',
aggfunc='sum', observed=True)
Making the column a Categorical with a defined order fixes the sorting everywhere in pandas, not only in this pivot: it also fixes groupby order and chart axis order. It is the right fix rather than reindexing the result afterwards.
Getting back to long
wide = df.pivot_table(index='region', columns='month',
values='amount', aggfunc='sum').reset_index()
long = wide.melt(id_vars='region', var_name='month', value_name='amount')
Wide is for reading, long is for computing. Charting libraries, databases and further groupbys all want long. Pivot at the last possible moment, immediately before a human looks at it.
pivot, pivot_table and groupby
| Use | When | Note |
|---|---|---|
groupby | Aggregating, staying long | The default choice |
pivot_table | Aggregating and going wide | Handles duplicates by aggregating them |
pivot | Reshaping only, no aggregation | Raises if the index and columns pair repeats |
crosstab | Counting combinations of two fields | Shorthand for a count pivot |
pivot failing on duplicates is a feature. It means your key was not unique, which is the same finding as fan-out in a SQL join, and it is better to hear about it than to have it silently averaged.
How to apply this to your own work
- Pass
aggfuncexplicitly in every pivot you write, even when the default happens to be right. - Check the grand total against the ungrouped sum before believing any pivot.
- Decide consciously whether a blank cell is a zero or an unknown.
- Set month and other ordered text columns to Categorical once, at load time.
- Keep the data long through the pipeline and pivot only for display.
The one habit to keep
Reconcile the corner. The bottom right cell of a pivot with margins should equal the sum of the original column, and if it does not, something was filtered, dropped or averaged on the way.
Does the grand total of your last pivot match the raw column total?
Python for Analysts is a working analyst pandas book: read a file, fix its types, group it, join it, and check the answer before anyone else sees it.
Python for Analysts, $19 →pandas groupby is the other half of the same idea, Excel pivot tables is the version most readers already know, and the drill routine puts both into practice.
Read pandas groupby →