One question, three tools
"How many rows fall into each combination of category A and category B?" is one of
the most common analytics questions — and pandas has three ways to answer it:
groupby, pivot_table, and crosstab. They overlap on purpose; picking the right
one is mostly about which shape of answer you want.
Example dataset
import pandas as pd
survey = pd.DataFrame({
"nationality": ["USA", "Japan", "USA", "Japan", "Japan", "USA"],
"handedness": ["Right", "Left", "Right", "Right", "Left", "Right"],
})
print(survey)crosstab: counting combinations
pd.crosstab(rows, columns) counts how many times each combination of two (or more)
categorical values occurs — no values= or aggfunc= needed, it's frequency by
default:
print(pd.crosstab(survey["nationality"], survey["handedness"]))Adding row/column totals with margins
print(pd.crosstab(survey["nationality"], survey["handedness"], margins=True))The "All" row and column are the totals across that dimension — 3 Japanese
respondents, 3 American respondents, 6 total.
Normalizing into proportions
Raw counts don't tell you whether a pattern is proportionally strong. Pass
normalize="index" to turn each row into percentages that sum to 1:
print(pd.crosstab(survey["nationality"], survey["handedness"], normalize="index"))Now you can see that 100% of the American respondents in this sample are right-handed, versus about 33% of the Japanese respondents.
pivot_table: aggregating a numeric column by category
pivot_table answers a related but different question: not "how many rows,"
but "what's the sum (or mean, or any aggregate) of some numeric column,"
broken down by category:
sales = pd.DataFrame({
"city": ["pune", "pune", "delhi", "delhi", "delhi", "pune"],
"category": ["A", "B", "A", "A", "B", "A"],
"amount": [100, 200, 150, 120, 300, 80],
})
summary = sales.pivot_table(index="city", columns="category", values="amount", aggfunc="sum")
print(summary)margins and multiple aggregations
Just like crosstab, pivot_table accepts margins=True for row/column totals. It
also accepts a list of aggregation functions to compute several statistics at once:
print(sales.pivot_table(index="city", columns="category", values="amount", aggfunc="sum", margins=True))print(sales.pivot_table(index="city", values="amount", aggfunc=["sum", "mean"]))crosstab vs pivot_table vs groupby
| Tool | Best for | Default aggregation |
|---|---|---|
groupby(...).agg(...) | Full control, any number of metrics/columns | You choose |
pivot_table | A numeric column, aggregated by 1-2 categorical keys, arranged as a grid | "mean" |
crosstab | Just counting how often category combinations occur | "count" |
Anything crosstab can do, pivot_table(..., aggfunc="count") can also do — reach
for crosstab when you specifically want frequencies, since it reads more clearly.
Common pitfalls
pivot_table's defaultaggfuncis"mean"— if you actually want totals, you must passaggfunc="sum"explicitly, or you'll get a quietly wrong answer.- Empty (index, column) combinations become
NaNunless you passfill_value=0. margins=True's"All"row/column is computed over the entire dataset, not just an average of the other rows/columns you can see — don't average it in by hand.
Visualize it
flowchart LR D["Rows split by category"] --> G["groupby().agg()
your choice of metrics"] D --> P["pivot_table()
numeric column, grid of categories"] D --> C["crosstab()
frequency grid of categories"]
🧪 Try It Yourself
Exercise 1 – Count Combinations With crosstab
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 2 – Sum a Numeric Column With pivot_table
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 3 – Add Grand Totals With margins
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Check your understanding
What is the default aggregation of pivot_table?
pivot_table defaults to mean, so you must pass aggfunc="sum" explicitly if you want totals. crosstab defaults to counting.
Show answer
C — mean — pivot_table defaults to mean, so you must pass aggfunc="sum" explicitly if you want totals. crosstab defaults to counting.
In the survey data, 3 of the 6 respondents are Japanese and 1 of them is right-handed. What does normalize="index" show for Japan under Right?
normalize=index turns each row into proportions that sum to 1. One of three Japanese respondents is right-handed, so 0.33.
Show answer
B — About 0.33 — normalize=index turns each row into proportions that sum to 1. One of three Japanese respondents is right-handed, so 0.33.
What does margins=True add to a crosstab or pivot_table?
The All row and column hold totals computed over the entire dataset.
Show answer
A — An All row and column with totals — The All row and column hold totals computed over the entire dataset.
For the sales example, which call gives sum of amount with city as rows and category as columns?
pivot_table aggregates a numeric column by category keys. For example, delhi and category A sums to 150 + 120 = 270.
Show answer
A — sales.pivot_table(index="city", columns="category", values="amount", aggfunc="sum") — pivot_table aggregates a numeric column by category keys. For example, delhi and category A sums to 150 + 120 = 270.
Next
That wraps up grouping and summarizing. Continue to Hierarchical Indexing (MultiIndex) to see how a two-level row index gives you similar structure without reshaping into a grid at all.