Python Central HubSign in

Data Analytics with Python / Phase 3: Data Manipulation with Pandas

Cross-Tabulation and Pivot Table Depth

Summarize two or more categorical columns at once with pd.crosstab (counts, margins, normalize) and pandas.pivot_table (aggfunc, margins, multiple values) - and know when to reach for groupby instead.

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

Python
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:

Python
print(pd.crosstab(survey["nationality"], survey["handedness"]))

Adding row/column totals with margins

Python
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:

Python
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:

Python
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:

Python
print(sales.pivot_table(index="city", columns="category", values="amount", aggfunc="sum", margins=True))
Python
print(sales.pivot_table(index="city", values="amount", aggfunc=["sum", "mean"]))

crosstab vs pivot_table vs groupby

ToolBest forDefault aggregation
groupby(...).agg(...)Full control, any number of metrics/columnsYou choose
pivot_tableA numeric column, aggregated by 1-2 categorical keys, arranged as a grid"mean"
crosstabJust 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 default aggfunc is "mean" — if you actually want totals, you must pass aggfunc="sum" explicitly, or you'll get a quietly wrong answer.
  • Empty (index, column) combinations become NaN unless you pass fill_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

diagram Same split, three different outputs mermaid
groupby, pivot_table, and crosstab all split rows by category, but each arranges and summarizes the result differently.
sketch A contingency grid: counts per combination p5.js
Each cell counts how many survey rows share that row category and that column category - the core idea behind crosstab.

🧪 Try It Yourself

Exercise 1 – Count Combinations With crosstab

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 2 – Sum a Numeric Column With pivot_table

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 3 – Add Grand Totals With margins

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Check your understanding

quizCheck yourself
  1. What is the default aggregation of pivot_table?

    Show answer

    C — mean — pivot_table defaults to mean, so you must pass aggfunc="sum" explicitly if you want totals. crosstab defaults to counting.

  2. 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?

    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.

  3. What does margins=True add to a crosstab or pivot_table?

    Show answer

    A — An All row and column with totals — The All row and column hold totals computed over the entire dataset.

  4. For the sales example, which call gives sum of amount with city as rows and category as columns?

    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.

Was this page helpful?

Let us know how we did