Q&A library Python

How do I build a frequency table of two categories in pandas?

Python Intermediate categoricalcrosstabfrequency

Short answer

Use crosstab to count how often each combination of two categories occurs, laid out as a grid. Add normalize for proportions and margins for row and column totals.

The short version

Use pd.crosstab(rows, columns) to count how often each combination of two categorical columns occurs, arranged as a grid with one category down the side and the other across the top. Add normalize=True for proportions instead of counts, and margins=True to include row and column totals.

A worked example

import pandas as pd

df = pd.DataFrame({
    "city": ["Leeds", "Leeds", "Bath"],
    "size": ["S", "L", "S"],
})

print(pd.crosstab(df["city"], df["size"]))
size   L  S
city
Bath   0  1
Leeds  1  1

Each cell counts the rows with that city and size combination. Bath has one small and no large; Leeds has one of each.

Proportions instead of counts

normalize turns counts into shares. normalize="index" makes each row sum to 1, "columns" makes each column sum to 1, and True divides by the grand total.

pd.crosstab(df["city"], df["size"], normalize="index")

This answers questions like "what proportion of Leeds rows are large", which raw counts do not show directly.

Adding totals

margins=True adds an "All" row and column with the totals, handy for a quick sanity check that the parts sum correctly.

pd.crosstab(df["city"], df["size"], margins=True)

crosstab versus value_counts versus pivot_table

value_counts counts one column. crosstab is the natural tool for counting the interaction of two columns. pivot_table is the more general tool that can do the same and more, aggregating a third value column with any function; crosstab is really a convenient special case of it for counting. Reach for crosstab when you specifically want a count grid of two categories, and pivot_table when you need to summarise a numeric column across them.

Aggregating a value instead of counting

If you want to summarise a numeric column rather than count rows, pass values and aggfunc, which pushes crosstab towards pivot_table territory.

pd.crosstab(df["city"], df["size"], values=df["amount"], aggfunc="mean")

A note on missing combinations

Combinations that never occur show as 0, not as absent rows, which is usually what you want for a complete table. If a category value is itself missing (NaN), it is dropped by default; pass dropna=False to keep it as its own row or column.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project