Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Analysis with Python (pandas, polars, DuckDB)  Module 5 – Summaries, group-by and pivot tables

Pivot tables and crosstabs in pandas

Build spreadsheet-style pivot tables with totals in pandas, count pairs of labels with crosstab and its shares, and turn SUMIFS and COUNTIFS habits into code.

  • Beginner
  • 35 minutes
  • Examples run with Python 3.14.8 and pandas 3.0.6
  • By MySmartCoPilot
Free core, Pro depth: 14 of 35 minutes free
Free: Each grid cell reduces its matching source rows · A crosstab frequency and its share need a declared population · Matching predicates translate the shown spreadsheet criteria · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 21 minutes.

What you will learn

  • Build spreadsheet-style pivot tables with pivot_table, margins and fill_value
  • Compute two-way frequency tables with row or column shares using crosstab
  • Map SUMIFS, COUNTIFS and XLOOKUP habits to pandas operations
  • Decide when a pivot is a report and when tidy long data is better

Before you start

Sixteen authored sales rows total 15500, while a report grid with both margins sums to 62000. A pivot table: A grid whose cells summarize source rows sharing specified row and column keys, with a declared aggregation and missing-value policy. rearranges long format: A representation whose row grain is stated and whose measured variables remain in columns, rather than spreading a reporting dimension into separate columns. into a grid. You choose a column whose values become the rows, a column whose values become the columns, a column of numbers and a way to combine them, and every cell of the grid summarises the rows that share its row and column values. Pivot tables and crosstab: A table of combinations of categorical labels, commonly showing occurrence counts or shares under a declared denominator. in pandas reproduce the two summaries that the authored spreadsheet-style examples use. pivot_table(index, columns, values, aggfunc) builds the grid, with margins=True for totals and fill_value=0 for empty cells. pd.crosstab(a, b) counts how often each pair of labels occurs, and its normalize option turns the counts into shares of each row, each column or the grand total. For the shown literal-equality criteria and declared types, SUMIFS and COUNTIFS can be expressed as matching pandas operations, and one rule keeps the analysis correct: pivot at the end, for reading, and keep the data itself as long rows.

Each grid cell reduces its matching source rows

The authored 16-row sales fixture spans four branches and four calendar quarters: one sale per row, with its branch, its date and its revenue.

sales.csv: 16 sales of four branches CSV · sales.csv
branch,date,revenue
North,2026-01-12,1200
South,2026-01-20,900
East,2026-02-03,700
North,2026-02-25,800
South,2026-03-15,1100
North,2026-04-08,1500
East,2026-04-22,650
West,2026-05-05,400
South,2026-05-19,1000
North,2026-07-02,1300
West,2026-07-21,550
East,2026-08-14,900
South,2026-09-09,1250
North,2026-10-06,1600
West,2026-11-11,700
South,2026-12-01,950
pivot_table turns 16 long rows of sales into a grid of branches by quarters, summing each cell and adding totals.From rows to a grid16 sale rowsbranch and dateRows: branchColumns: quarterNorth Q11200 and 800Declared sumNorth Q1: 2000West Q1no sale rowsKnown empty cellfill_value=0One branch rowquarter columnsBoth marginscorner: 15500

From long rows to a branch-by-quarter grid

Text description of the diagram

The input has one sale per row, with its branch, its quarter and its revenue: for example North in Q1 with 1,200, North in Q1 with 800, and West in Q2 with 400.

pivot_table is called with the branch as the index, the quarter as the columns, the revenue as the values and sum as the aggregation.

The result is a grid with one row per branch and one column per quarter:

  1. Each cell adds up the sales that share its branch and quarter. North's two sales in Q1, 1,200 and 800, make a cell of 2,000.
  2. West had no sales in Q1, so its cell has nothing to add up; fill_value=0 writes 0 there instead of leaving it empty.
  3. margins=True adds a total row and a total column, computed from the rows themselves. The corner, the total of all 16 sales, is 15,500.

Zero filling here represents a known empty source cell. It is not a declaration that every unrecorded observed amount was zero. The grid combines transaction values, so a later reshape preserves its aggregate cells rather than recovering individual source sales.

Revenue by branch and quarter, with totals Python · branch_quarter.py
import pandas as pd

sales = pd.read_csv("sales.csv", parse_dates=["date"])  # 16 sales of four branches over one year
sales["quarter"] = "Q" + sales["date"].dt.quarter.astype(str)

grid = sales.pivot_table(
    index="branch",        # one row per branch
    columns="quarter",     # one column per quarter
    values="revenue",      # the numbers in the cells
    aggfunc="sum",         # how to combine the rows of a cell
    fill_value=0,          # a cell with no sales shows 0
    margins=True,          # add totals
    margins_name="Total",  # instead of the default "All"
)
print(grid)

Output

quarter    Q1    Q2    Q3    Q4  Total
branch                                
East      700   650   900     0   2250
North    2000  1500  1300  1600   6400
South    2000  1000  1250   950   5200
West        0   400   550   700   1650
Total    4700  3550  4000  3250  15500

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 branch_quarter.py

Each cell adds up the sales that share its branch and quarter: North sold 1,200 and 800 in the first quarter, so its Q1 cell is 2,000. West opened in the second quarter and East sold nothing in the last one, and fill_value=0 writes 0 where the grid would otherwise show a missing value. Zero is justified here by a known empty cell in this authored fixture; it is not a universal replacement for unrecorded revenue. margins=True adds a total row and a total column, computed from the rows themselves, and margins_name renames them from the default “All”. Write aggfunc out every time. Its default is the mean, which is rarely what a grid of revenue should show, and a grid of means looks just as plausible as a grid of sums. Quarters of a financial year that does not end in December come from the date column too, for example with dt.to_period("Q-MAR") for a year that ends in March.

Interactive · stage by stage North's two first-quarter sales make one aggregate cell Use branch_quarter.py and name the aggregation before reading the grid.
North's two first-quarter sales make one aggregate cell, step 3 of 3. Showing Matching long rows, Declared reduction, A reporting grid. In focus: A reporting grid.Matching long rowsNorth Q1:1200 and 800Declared reductionsum:2000; mean:1000A reporting gridcorner total:15500

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Matching long rows. Two source rows share the North branch and first-quarter label.
  2. Declared reduction. A sum cell is 2000, while a mean cell would be 1000. Both can look plausible, so the report must name its aggregation.
  3. A reporting grid. The sum pivot's margin corner matches the recorded source total 15500. This agreement is an explicit source control.

Key idea

A cell needs its grain, aggregation and missing-value contract stated.

Checkpoint

Check: Grid grain and aggregation

  1. One question North's Q1 source amounts are 1200 and 800. What does a sum cell contain?

    Type a number.

    Show the answer: One question

    Answer: 2000 amount units

    The cell combines both matching source rows:1200+800=2000.

A crosstab frequency and its share need a declared population

Admissions by course and mode of study, counted and as row shares Python · admissions_crosstab.py
import pandas as pd

admissions = pd.read_csv("admissions.csv")  # 20 admissions: the course and the mode of study

counts = pd.crosstab(admissions["course"], admissions["mode"], margins=True, margins_name="Total")
print(counts)
print()
shares = pd.crosstab(admissions["course"], admissions["mode"], normalize="index")  # each row adds up to 1
print(shares.round(3))

Output

mode          full-time  online  part-time  Total
course                                           
Commerce              5       1          2      8
Data science          2       5          1      8
Design                3       0          1      4
Total                10       6          4     20

mode          full-time  online  part-time
course                                    
Commerce          0.625   0.125      0.250
Data science      0.250   0.625      0.125
Design            0.750   0.000      0.250

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 admissions_crosstab.py

Crosstabs in pandas count occurrences of label combinations. Crosstabs in pandas can also normalize those counts into shares of a declared row, column or whole-table denominator. Called as pd.crosstab(course, mode), it gives the number of admissions for each pair of course and mode of study, and margins=True adds the totals: 20 admissions, 8 of them in Commerce. With normalize="index" every row is divided by its own total, so each row adds up to 1. That reads as “how do the students of each course study?”: 62.5 % of Commerce students study full-time, while 62.5 % of Data science students study online. normalize="columns" and normalize="all" divide by the column totals and by the grand total instead. Pivot tables can count as well, with aggfunc="size", but a crosstab says what it does in one call. Its grand total is the number of rows in which both labels are present, which is a useful count control for this declared complete-label population. It does not by itself prove exact source-row identity.

Interactive · stage by stage Five of eight Commerce rows use the full-time label Read admissions_crosstab.py with the count table beside its normalized table.
Five of eight Commerce rows use the full-time label, step 3 of 3. Showing The fixture population, A cell count, A row-normalized share. In focus: A row-normalized share.The fixture population20 rows; Commerce 8A cell countCommerce full-time:5A row-normalized share5/8=62.5 percent

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. The fixture population. The authored admissions fixture has twenty rows, eight labelled Commerce, with both labels present.
  2. A cell count. Five Commerce rows carry the full-time mode label.
  3. A row-normalized share. Dividing by the Commerce row's eight observations gives 62.5 percent. A column or whole-table denominator answers another question.

Key idea

A percentage is incomplete without the population and denominator it uses.

Checkpoint

Check: Frequency-table denominators

  1. One question Five of 8 Commerce rows have the full-time label. What is their row share as a percentage?

    Type a number.

    Show the answer: One question

    Answer: 62.5 percent

    5/8×100=62.5 percent.

Matching predicates translate the shown spreadsheet criteria

Spreadsheet pandas Where it is taught
SUMIFS df.loc[mask, "revenue"].sum(), or groupby(keys)["revenue"].sum() for every combination This module
COUNTIFS groupby(keys).size() or value_counts() This module
AVERAGEIFS groupby(keys)["revenue"].mean() This module
A pivot’s percentage of the row total pd.crosstab(..., normalize="index") This lesson
XLOOKUP An exact lookup can use merge(..., how="left") after its key and multiplicity rules are aligned The joins module
Conditional formatting Styler The reporting module

The source and matching contracts matter more than a superficial syntax translation. Confirm wildcard, blank, case and lookup-mode behaviour before generalising beyond these literal-equality examples. A spreadsheet formula answers one cell, while a group-by answers every combination at once, so one line of pandas replaces a column of copied formulas, and a new branch appears in the result without anyone extending a range.

Interactive · stage by stage The North first-quarter equality predicates intersect Follow sumifs_check.py before extending its equality matcher to other formula modes.
The North first-quarter equality predicates intersect, step 3 of 3. Showing First criterion, Second criterion, Sum or count the match. In focus: Sum or count the match.First criterionbranch equals NorthSecond criterionquarter equals Q1Sum or count the matchsum 2000; row count 2

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. First criterion. The first equality predicate selects the North source rows.
  2. Second criterion. The second predicate selects first-quarter rows. Combining them with AND selects the intersection.
  3. Sum or count the match. The two matched rows sum to 2000, while counting them gives 2. Different formula matching modes need a separately verified contract.

Key idea

Translate the declared criteria and aggregation, rather than assuming every spreadsheet formula is a plain equality join.

Checkpoint

Check: Literal criteria translation

  1. One question What combines the two literal equality criteria in the shown SUMIFS matcher?

    Choose one answer.

    Show the answer: One question

    Answer: AND

    Match the stated criteria before summing or counting.

A report grid and source rows serve different calculations

The shown pivot is a reporting representation. Keep the data long, one row per sale, while you clean, join, filter and summarise it, and usually pivot at the reporting boundary: a grid has one column per quarter, and every step that follows has to know those columns. When data arrives already pivoted, melt turns the grid back into long rows. The representation also defines what later calculations can recover: a cell has no individual sale identifier or within-cell distribution. Keep the transaction source for later joins or statistics that need those observations. A pivot that goes into a report should say in its title what each cell holds, such as “revenue, sum, by branch and quarter”, because a grid of sums and a grid of means look exactly alike.

Interactive · stage by stage The source retains sale identity beyond a grouped grid Keep the long sales fixture beside the grid and state each table's row grain.
The source retains sale identity beyond a grouped grid, step 3 of 3. Showing Transaction grain, Grouped grid grain, Representation change. In focus: Representation change.Transaction grainone row per sale;16 rowsGrouped grid grainone branch-quarter cellRepresentation changeaggregate rows after melt

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Transaction grain. The source retains each of sixteen authored sale rows.
  2. Grouped grid grain. A sum pivot combines all rows belonging to the same branch-quarter cell.
  3. Representation change. Melting the grid returns one row per cell, not one row per original transaction. Preserve the source when later work needs individual sales.

Key idea

Reshaping a grid cannot reverse an aggregation that discarded within-cell detail.

Checkpoint

Check: Representation and source grain

  1. One question A North Q1 pivot cell contains 2000. Can melting alone recover its original two amounts?

    Choose one answer.

    Show the answer: One question

    Answer: No; the sum discarded within-cell detail

    Preserve the transaction source when later work needs transaction grain.

Practise the reading core

  • In branch_quarter.py, request a mean; compare each populated cell with the sum report.
  • In admissions_crosstab.py, switch the denominator to columns; name the changed question.

Practice that adapts to you

Practise this lesson

8 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 sales.pivot_table(index="branch", columns="quarter", values="revenue") has no aggfunc. What does each cell hold?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: The mean revenue of the sales in that branch and quarter

    pivot_table aggregates with the mean unless you pass aggfunc. For revenue you almost always want aggfunc="sum", so write it out every time; the result of the default looks plausible and is easy to mistake for totals.

  2. Question 2 of 8 In pd.crosstab(course, mode, normalize="index"), what adds up to 1?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: Each row, so every course's modes of study add up to 1

    "index" divides each row by the row's own total, which answers "how do students of this course study?". normalize="columns" divides by column totals and normalize="all" by the grand total.

  3. Question 3 of 8 Which pandas expression gives the same number as the spreadsheet formula SUMIFS(revenue, branch, "North", quarter, "Q1")?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: sales.loc[(sales["branch"] == "North") & (sales["quarter"] == "Q1"), "revenue"].sum()

    SUMIFS adds the values whose rows meet every condition, so the conditions are combined with & (and), not | (or). Grouping by branch alone ignores the quarter, and count() counts rows instead of adding revenue. To get every branch and quarter at once, group by both columns or build the pivot.

  4. Question 4 of 8 The two North Q1 amounts are 1200 and 800. What is the sum cell?

    Type a number.

    Show the answer: Question 4 of 8

    Answer: 2000 amount units

    1200+800=2000; the mean cell would be 1000.

  5. Question 5 of 8 Commerce has 8 observations and 5 full-time labels. What is the full-time row share?

    Type a number.

    Show the answer: Question 5 of 8

    Answer: 62.5 percent

    5/8×100=62.5 percent.

  6. Question 6 of 8 What does the shown AND predicate select before a sum?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: Rows satisfying both exact stated criteria

    Verify matching semantics rather than translating the formula name alone.

  7. Question 7 of 8 What is one row of the long transaction source in this fixture?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: One authored sale

    Name the grain of each table.

  8. Question 8 of 8 Two distinct transaction pairs both sum to the same pivot cell. What does a melt retain?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: The same aggregate cell value

    Preserve the original rows when identity or within-cell distribution matters.

Pivot tables that match the spreadsheet cell by cell, with the traps reproduced Pro: opens with a Pro, Premium or Ultimate pass

A pivot is trusted when its cells, its totals and its layout match what the reader’s spreadsheet shows. This part checks a pivot against SUMIFS and COUNTIFS formulas cell by cell, reproduces the totals that differ when the cells are averages, and fixes the order of the columns and the rows that go missing.

In this part:

  • Each fixture cell can be compared with its matching source rows
  • A mean margin uses observations rather than equally weighted cells
  • A declared universe fixes empty categories and column order
  • Quarter labels follow the chosen financial-year ending month

4 diagrams · 4 runnable examples (Python) · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Reports or analysis tables: summing a pivot, going back to long rows, and shares Pro: opens with a Pro, Premium or Ultimate pass

For this reporting workflow, build the pivot at the output boundary. A wide matrix can be appropriate for a different calculation when its grain and columns are explicitly defined. This part reproduces the most expensive mistake made with a finished pivot, reshapes its aggregate cells into long rows while keeping the lost transaction detail explicit, and compares the three kinds of share that a crosstab gives.

In this part:

  • Including both margins counts this additive report repeatedly
  • Melting preserves grouped cells and their missing-cell policy
  • Three denominators describe three questions about the same counts
  • Practise a collection report from raw rows

3 diagrams · 7 runnable examples (CSV, Python) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher · Warm-up

    When do you use pd.crosstab instead of pivot_table, and what does each one take as input?

    pivot_table works on a DataFrame: you name the columns for the rows, the columns and the values, and it combines the values of each cell with aggfunc, so it answers "how much" questions such as revenue by branch and quarter. crosstab takes two or more Series or arrays and by default counts how often each combination of their values occurs, so it answers "how often" questions such as admissions by course and mode of study. crosstab can also aggregate a values array with aggfunc, and pivot_table can count with aggfunc="size", but each is shorter for its own job. Only crosstab has normalize, which turns the counts into shares of each row, each column or the grand total.

  2. Fresher to mid

    Your pivot of average order value by city and month has an All column. A manager averages the twelve monthly cells of one city, gets a different number from its All cell, and says the report is wrong. Who is right, and how do you explain it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Fresher to mid

    A slide says the year's revenue was 3.1 million, but finance reports 1.55 million. The slide's figure came from adding up a column of a pivot report copied out of your notebook. What happened, and how do you build reports so that it cannot happen again?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    An analyst's spreadsheet model builds a monthly report with SUMIFS, COUNTIFS and XLOOKUP across three sheets, and it takes 20 minutes to recalculate. How would you move it to pandas, and how do you prove that the new numbers match the old ones?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • pivot_table(index, columns, values, aggfunc="sum") builds the grid; margins=True adds totals and fill_value=0 fills empty cells.
  • Always pass aggfunc: the default is the mean.
  • pd.crosstab(a, b) counts pairs of labels, and normalize="index" gives shares that add up to 1 per row.
  • With the shown literal-equality criteria and declared types, SUMIFS, COUNTIFS and AVERAGEIFS map to grouped summaries of matching rows.
  • Pivot at the end, for reading; keep the data as long rows while you analyse it.

Terms in this lesson

crosstab
A table of combinations of categorical labels, commonly showing occurrence counts or shares under a declared denominator.
long format
A representation whose row grain is stated and whose measured variables remain in columns, rather than spreading a reporting dimension into separate columns.
pivot table
A grid whose cells summarize source rows sharing specified row and column keys, with a declared aggregation and missing-value policy.

Corrections

  • Clarified statistical inference and source-row or reporting contracts; existing explanations remain in their original reading parts.
  • Qualified lookup modes, aggregate reshaping and diagnostic revenue ratios.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.