pandas crosstab in Python: Counts, Totals and Sums

A pandas crosstab counts how often each pair of values shows up: call pd.crosstab(rows, columns) with two columns and you get a frequency table, one row per unique value of the first and one column per unique value of the second.

pd.crosstab(df["region"], df["category"])

Every table below came from a real run on Python 3.12.5, pandas 3.0.6 and Matplotlib 3.11.2. The data is a small orders file: 12 Q1 2026 orders across three US regions, with amounts in USD.

In this tutorial, we’ll build count tables, add totals, sum and average dollar amounts, nest several columns, and check when crosstab beats pivot_table or groupby.

The sample orders data for these crosstab examples

Save this as orders.csv next to your script. If you’d rather build it in code, see creating a pandas DataFrame from a dict.

order_date,region,category,segment,amount
2026-01-05,East,Technology,Consumer,1249.99
2026-01-09,West,Furniture,Corporate,689.50
2026-01-14,East,Office Supplies,Consumer,45.20
2026-01-21,South,Technology,Corporate,899.00
2026-02-02,West,Technology,Consumer,329.99
2026-02-11,South,Furniture,Consumer,415.75
2026-02-17,East,Furniture,Corporate,1120.00
2026-02-25,West,Office Supplies,Corporate,88.40
2026-03-04,South,Technology,Consumer,189.99
2026-03-12,East,Technology,Corporate,2199.00
2026-03-19,West,Office Supplies,Corporate,64.75
2026-03-27,South,Technology,Consumer,159.99

Count combinations with a basic pd.crosstab

Pass the row labels first and the column labels second. Each cell is the number of orders with that region and category:

import pandas as pd

df = pd.read_csv("orders.csv")
print(pd.crosstab(df["region"], df["category"]))

Output:

category  Furniture  Office Supplies  Technology
region                                          
East              1                1           2
South             1                0           3
West              1                2           1
Command Prompt output of a basic pandas crosstab counting orders by region and category
South has no Office Supplies orders, so that cell is 0, and Technology is the busiest column with six orders.

It’s the two-column version of counting unique values in a pandas column, and it saves you a groupby plus an unstack.

Add row and column totals with margins

margins=True adds a total row and column. I rename it with margins_name, and rownames/colnames label the axes:

import pandas as pd

df = pd.read_csv("orders.csv")
ct = pd.crosstab(df["region"], df["category"],
                 margins=True, margins_name="Total",
                 rownames=["Region"], colnames=["Category"])
print(ct)

Output:

Category  Furniture  Office Supplies  Technology  Total
Region                                                 
East              1                1           2      4
South             1                0           3      4
West              1                2           1      4
Total             3                3           6     12
Command Prompt output of a pandas crosstab with margins showing Total row and column
The Total column gives four orders per region, and the corner cell confirms all 12 orders.

To check that grand total against the raw data, counting rows in a pandas DataFrame gives the same 12.

Sum or average a column with values and aggfunc

By default crosstab counts. Pass a numeric column as values plus an aggfunc such as "sum" or "mean", and you’ll get dollars instead of counts. You can’t use one without the other:

import pandas as pd

pd.set_option("display.precision", 2)   # show USD with 2 decimals
df = pd.read_csv("orders.csv")
sales = pd.crosstab(df["region"], df["category"], values=df["amount"],
                    aggfunc="sum", margins=True, margins_name="Total")
print(sales)
print()
print(pd.crosstab(df["region"], df["segment"], values=df["amount"], aggfunc="mean"))

Output:

category  Furniture  Office Supplies  Technology    Total
region                                                   
East        1120.00            45.20     3448.99  4614.19
South        415.75              NaN     1248.98  1664.73
West         689.50           153.15      329.99  1172.64
Total       2225.25           198.35     5027.96  7451.56

segment  Consumer  Corporate
region                      
East       647.60    1659.50
South      255.24     899.00
West       329.99     280.88
Command Prompt output of pandas crosstab summing and averaging order amounts in USD by region
East leads Q1 2026 with $4,614.19 in sales, and South’s Office Supplies cell is NaN because it had no such orders.

Any combination with no rows comes back as NaN, not 0; replacing NaN with 0 in pandas fixes that. For several functions at once, see pandas groupby aggregation functions.

Build a crosstab with multiple index columns

Pass a list of columns to get a MultiIndex, meaning nested row labels. Here rows are region plus segment, and the columns are the order month pulled from the date:

import pandas as pd

df = pd.read_csv("orders.csv", parse_dates=["order_date"])
month = df["order_date"].dt.strftime("%m-%b")
print(pd.crosstab([df["region"], df["segment"]], month))

Output:

order_date        01-Jan  02-Feb  03-Mar
region segment                          
East   Consumer        2       0       0
       Corporate       0       1       1
South  Consumer        0       1       2
       Corporate       1       0       0
West   Consumer        0       1       0
       Corporate       1       1       1
Command Prompt output of a pandas crosstab with region and segment as rows and order month as columns
Region and segment form nested row labels; the zero cells show months where that pair had no orders.

Lists work for columns too. Reading dates straight from the file is covered in reading a CSV file with pandas.

Show crosstab percentages with normalize

normalize="index" turns each row into shares that add up to 1; "columns" and True work the same way down columns or over the whole table:

import pandas as pd

df = pd.read_csv("orders.csv")
print(pd.crosstab(df["region"], df["category"], normalize="index").round(2))

Output:

category  Furniture  Office Supplies  Technology
region                                          
East           0.25             0.25        0.50
South          0.25             0.00        0.75
West           0.25             0.50        0.25

Each region’s row adds up to 1.0. I go deeper, with margins and formatting, in percentage normalization using crosstab in pandas.

When should you use crosstab instead of pivot_table or groupby?

All three give the same sums here. I’ve checked it, and both comparisons print True:

import pandas as pd

df = pd.read_csv("orders.csv")
ct = pd.crosstab(df["region"], df["category"], values=df["amount"], aggfunc="sum")
pt = df.pivot_table(index="region", columns="category", values="amount", aggfunc="sum")
gb = df.groupby(["region", "category"])["amount"].sum().unstack()

print(ct.equals(pt), ct.equals(gb))

Output:

True True

Reach for crosstab for counts and for arrays or Series that aren’t in one DataFrame. Use a pandas pivot table when the data’s already a DataFrame, and groupby in pandas for long, tidy results.

Plot a pandas crosstab as a stacked bar chart

A crosstab is already a DataFrame, so .plot() draws one bar per row and stacks the columns:

import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("orders.csv")
ct = pd.crosstab(df["region"], df["category"], values=df["amount"], aggfunc="sum")

ax = ct.plot(kind="bar", stacked=True, figsize=(8, 4.5), rot=0)
ax.set_ylabel("Sales (USD)")
ax.set_title("Q1 2026 sales by region and category")
plt.tight_layout()
plt.show()
Matplotlib window with a stacked bar chart of a pandas crosstab of Q1 2026 sales by region and category
Each bar is a region, and the colored stacks split its Q1 2026 sales by category.

More chart types, including heatmaps, are in creating plots using pandas crosstab.

Next, you could try one of these:

Frequently asked questions

What does pd.crosstab do?

It builds a frequency table: how often each combination of two or more factors occurs.

How do I add totals to a pandas crosstab?

Pass margins=True; rename the label with margins_name="Total".

Can crosstab sum a column instead of counting?

Yes. Pass values=df["amount"] together with aggfunc="sum".

Why does my crosstab show NaN?

With values, empty combinations become NaN. Add .fillna(0).

Is crosstab faster than pivot_table?

Not really; crosstab calls pivot_table internally. Pick whichever reads better.

Where are all the crosstab parameters documented?

See pandas.crosstab in the pandas docs.