crosstab for Frequency Tables
Compute a frequency or proportion cross-tabulation of two categorical columns with pd.crosstab.
crosstab for Frequency Tables is a free Pandas & NumPy Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Pandas & NumPy Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is a Crosstab?
A cross-tabulation (crosstab) counts how many times each combination of values from two (or more) categorical variables appears in a dataset. It is a special case of pivot table designed specifically for counting. Pandas provides pd.crosstab() as a concise way to compute these frequency tables without needing to call groupby() or pivot_table() explicitly.
Basic crosstab() Call
The minimal pd.crosstab(index, columns) call takes two array-like inputs (typically DataFrame columns) and returns a frequency table. Rows correspond to unique values in the first argument and columns correspond to unique values in the second. Each cell contains the count of rows where both values co-occur in the original data.
import pandas as pd
df = pd.DataFrame({
'gender': ['M', 'F', 'M', 'F', 'M', 'F', 'M'],
'product': ['A', 'A', 'B', 'B', 'A', 'B', 'B']
})
ct = pd.crosstab(df['gender'], df['product'])
print(ct)
# product A B
# gender
# F 1 2
# M 2 2Customising Row and Column Names
Use the rownames and colnames parameters to give the row and column axis meaningful names in the output, overriding the default Series names. This is useful when the raw column names in your DataFrame are not self-explanatory in the context of the table being produced.
ct = pd.crosstab(
df['gender'], df['product'],
rownames=['Customer Gender'],
colnames=['Purchased Product']
)
print(ct)
# Purchased Product A B
# Customer Gender
# F 1 2
# M 2 2Adding Margins (Row and Column Totals)
Pass margins=True to include a row and column totalling all values. The margin label defaults to 'All' but can be customised with margins_name. The margins row shows the total count per column, the margins column shows the total count per row, and the bottom-right cell shows the grand total.
ct = pd.crosstab(df['gender'], df['product'],
margins=True, margins_name='Total')
print(ct)
# product A B Total
# gender
# F 1 2 3
# M 2 2 4
# Total 3 4 7Normalising to Proportions
Pass the normalize parameter to convert counts to proportions. normalize=True or normalize='all' divides by the grand total. normalize='index' computes row proportions (each row sums to 1.0). normalize='columns' computes column proportions (each column sums to 1.0). Proportions are more informative than raw counts when group sizes differ.
# Row proportions: what fraction of each gender bought each product?
print(pd.crosstab(df['gender'], df['product'], normalize='index').round(2))
# product A B
# gender
# F 0.33 0.67
# M 0.50 0.50
# All proportions: each cell as fraction of grand total
print(pd.crosstab(df['gender'], df['product'], normalize=True).round(2))Aggregating Values Instead of Counting
By default, crosstab counts rows. You can instead aggregate a numeric column by passing values and aggfunc parameters — similar to pivot_table(). For example, compute total revenue for each gender-product combination. This makes crosstab nearly equivalent to pivot_table but with a slightly more concise syntax for the frequency table use case.
df['revenue'] = [100, 80, 150, 120, 200, 90, 130]
# Sum revenue by gender and product instead of counting
ct_rev = pd.crosstab(
df['gender'], df['product'],
values=df['revenue'],
aggfunc='sum'
)
print(ct_rev)
# product A B
# gender
# F 80 210
# M 300 280Multi-Level Crosstab
Pass a list to index or columns to create a crosstab with multiple levels on the rows or columns. The result has a MultiIndex, giving you a more granular breakdown. For example, cross-tabulating by gender and region on the rows against product on the columns shows every gender-region-product combination.
df['region'] = ['East', 'West', 'East', 'West', 'East', 'East', 'West']
ct_multi = pd.crosstab(
[df['gender'], df['region']],
df['product'],
margins=True
)
print(ct_multi)
# product A B All
# gender region
# F East 0 1 1
# West 1 1 2
# M East 2 1 3
# West 0 1 1
# All 3 4 7crosstab() vs pivot_table()
pd.crosstab() and pd.pivot_table() overlap significantly. crosstab is optimised for frequency counting and takes array-like inputs directly (not a DataFrame), making it slightly more concise for quick tabulations. pivot_table operates on a DataFrame and supports any aggregation function natively. For pure counting tasks, crosstab is the idiomatic choice; for aggregating numeric values, prefer pivot_table.
# crosstab (more concise for counting)
print(pd.crosstab(df['gender'], df['product']))
# Equivalent pivot_table
print(pd.pivot_table(df, index='gender', columns='product',
aggfunc='size', fill_value=0))
# Both produce the same resultChi-Squared Test Preparation
A crosstab is the standard input for a chi-squared test of independence, which tests whether two categorical variables are statistically related. The scipy.stats.chi2_contingency() function accepts a crosstab array directly. This is one of the most common uses of crosstab in statistical data analysis — you compute the table and then test it in one workflow.
from scipy import stats
ct = pd.crosstab(df['gender'], df['product'])
# Convert to numpy array for scipy
chi2, p, dof, expected = stats.chi2_contingency(ct.values)
print(f'Chi2: {chi2:.2f}')
print(f'P-value: {p:.4f}')
print(f'Degrees of freedom: {dof}')
# p > 0.05 means no significant associationVisualising Crosstab as a Heatmap
Crosstabs with many categories are hard to interpret as raw numbers. Visualising them as a heatmap with sns.heatmap() makes patterns immediately visible — high-frequency cells appear as bright colours. Pass the normalised version to show proportions rather than raw counts, and use annot=True to display the values in each cell.
import seaborn as sns
import matplotlib.pyplot as plt
ct_norm = pd.crosstab(df['gender'], df['product'], normalize='index')
# sns.heatmap(ct_norm, annot=True, fmt='.0%', cmap='Blues')
# plt.title('Purchase Rate by Gender and Product')
# plt.tight_layout()
# plt.show()
print('Normalised crosstab ready for heatmap:')
print(ct_norm.round(2))Practical Example: Survey Analysis
A complete crosstab workflow: load survey data, compute frequency tables between demographic and response variables, normalise by rows to get response rates, and identify any strong patterns. This is the standard analysis pipeline in market research, A/B testing, and user segmentation, and it can be completed in under 10 lines of Pandas code.
# Simulate a survey dataset
survey = pd.DataFrame({
'age_group': ['18-25', '26-35', '18-25', '36-45', '26-35', '36-45'],
'satisfaction': ['High', 'Low', 'High', 'Medium', 'High', 'Low']
})
ct = pd.crosstab(survey['age_group'], survey['satisfaction'],
margins=True, margins_name='Total')
print(ct)
rates = pd.crosstab(survey['age_group'], survey['satisfaction'],
normalize='index').round(2)
print(rates)Quick Check
Test your understanding of pd.crosstab for frequency tables from this lesson.
Lesson Recap
In this lesson you learned: pd.crosstab() computes frequency counts for combinations of two categorical variables; normalize converts counts to row, column, or total proportions; margins=True adds row and column totals; and crosstab output is directly compatible with chi-squared tests and heatmap visualisations. Next up we work with time series data using DatetimeIndex and period ranges.
Frequently asked questions
Is the “crosstab for Frequency Tables” lesson free?
Yes — the full text of “crosstab for Frequency Tables” is free to read here on the web, and the Pandas & NumPy Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Pandas & NumPy Academy course, upgrade to CoddyKit PRO.
What will I learn in “crosstab for Frequency Tables”?
Compute a frequency or proportion cross-tabulation of two categorical columns with pd.crosstab. You practise Pandas & NumPy Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Pandas & NumPy Academy?
No prior experience is required. Pandas & NumPy Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “crosstab for Frequency Tables” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Pandas & NumPy Academy lesson?
Yes. Every Pandas & NumPy Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- pivot_table: Cross-Tabulation
- melt: Wide to Long Format
- stack and unstack with MultiIndex
- crosstab for Frequency Tables