Pandasdrill

Grouping and aggregation Updated November 2026

pd.crosstab

pd.crosstab(df["a"], df["b"]) counts how many rows have each combination of a and b, returning a table with a values as rows and b values as columns. normalize="index" turns counts into row percentages and margins=True adds totals.

Syntax

pd.crosstab(index, columns, values=None, aggfunc=None, margins=False, normalize=False)
PartWhat it is
indexSeries (or list of Series) for the rows
columnsSeries (or list) for the columns
values / aggfuncAggregate a third column instead of counting rows
marginsTrue adds an All row and column
normalizeTrue, "index" or "columns" to show proportions

Examples

import pandas as pd

df = pd.DataFrame({"gender": ["F", "M", "F", "F", "M"],
                   "plan": ["free", "pro", "pro", "free", "free"],
                   "spend": [0, 20, 20, 0, 0]})
pd.crosstab(df["gender"], df["plan"])

Result: plan free pro gender F 2 1 M 1 1

pd.crosstab(df["gender"], df["plan"], margins=True)

Result: plan free pro All gender F 2 1 3 M 1 1 2 All 3 2 5

pd.crosstab(df["gender"], df["plan"], normalize="index").round(2)

Result: plan free pro gender F 0.67 0.33 M 0.50 0.50

Each row sums to 1: the share of plans within each gender.

Common mistakes

  • TypeError when passing column names as strings: crosstab takes Series, so write df["a"], not "a".
  • NaN in the table after using values: pass aggfunc too, or fill with .fillna(0).
  • Percentages look wrong: normalize=True divides by the grand total; use "index" for row shares or "columns" for column shares.

Practice pd.crosstab

These drills check your answer on live data, free, no account needed.

  • Counts with crosstabCount the orders for each region (rows) and product (columns) with `pd.crosstab`.

Questions

crosstab or pivot_table?

crosstab is a shortcut for counting combinations of two or more columns and supports normalize directly. pivot_table is more general for aggregating a values column.

How do I cross-tabulate three variables?

Pass a list for index or columns: pd.crosstab([df.a, df.b], df.c) gives a two-level row index.

Related

  • pivot_tableHow to build a pivot table in pandas with pivot_table: index, columns, values, aggfunc, fill_value for empty cells, margins for totals, several aggregations.
  • value_countsHow to count values in a pandas column with value_counts: counts sorted by frequency, percentages with normalize=True, including NaN, and two-column combos.
  • groupbyHow groupby works in pandas: group by one or more columns, apply sum, mean, count or size, get a flat table with as_index=False, and iterate over groups.
  • cut and qcutHow to bin numeric data in pandas: pd.cut for fixed ranges with labels like age bands, pd.qcut for equal-sized quantile groups, right-closed edges and labels.

Previous: value_countsNext: rolling