In pandas, two common tools for summarizing data are:
groupby() → group observations and calculate statistics.
pivot_table() → create spreadsheet-style summary tables.
import pandas as pd
df = pd.DataFrame({
"state": ["CDMX", "CDMX", "Jalisco", "Jalisco", "Nuevo León", "Nuevo León"],
"sex": ["M", "F", "M", "F", "M", "F"],
"sector": ["Public", "Public", "Private", "Public", "Private", "Private"],
"employees": [120, 150, 80, 110, 200, 170],
"salary": [25000, 28000, 22000, 24000, 32000, 30000]
})
df
| 0 |
CDMX |
M |
Public |
120 |
25000 |
| 1 |
CDMX |
F |
Public |
150 |
28000 |
| 2 |
Jalisco |
M |
Private |
80 |
22000 |
| 3 |
Jalisco |
F |
Public |
110 |
24000 |
| 4 |
Nuevo León |
M |
Private |
200 |
32000 |
| 5 |
Nuevo León |
F |
Private |
170 |
30000 |
1. Basic groupby()
General structure:
df.groupby("group_variable")["numeric_variable"].aggregation()
Sum
df.groupby("state")["employees"].sum()
state
CDMX 270
Jalisco 190
Nuevo León 370
Name: employees, dtype: int64
This returns a Series.
To return a DataFrame:
df.groupby("state", as_index=False)["employees"].sum()
| 0 |
CDMX |
270 |
| 1 |
Jalisco |
190 |
| 2 |
Nuevo León |
370 |
2. Count observations
size()
size() counts rows in each group.
df.groupby("state").size()
state
CDMX 2
Jalisco 2
Nuevo León 2
dtype: int64
A convenient DataFrame version:
df.groupby("state").size().reset_index(name="n")
| 0 |
CDMX |
2 |
| 1 |
Jalisco |
2 |
| 2 |
Nuevo León |
2 |
count()
count() counts non-missing values.
df.groupby("state")["salary"].count()
state
CDMX 2
Jalisco 2
Nuevo León 2
Name: salary, dtype: int64
The difference is:
size() -> counts rows
count() -> counts non-null values
3. Group using more than one variable
df.groupby(["state", "sex"])["employees"].sum()
state sex
CDMX F 150
M 120
Jalisco F 110
M 80
Nuevo León F 170
M 200
Name: employees, dtype: int64
Usually it is easier to work with the result using:
df.groupby(
["state", "sex"],
as_index=False
)["employees"].sum()
| 0 |
CDMX |
F |
150 |
| 1 |
CDMX |
M |
120 |
| 2 |
Jalisco |
F |
110 |
| 3 |
Jalisco |
M |
80 |
| 4 |
Nuevo León |
F |
170 |
| 5 |
Nuevo León |
M |
200 |
4. Aggregation with agg()
agg() allows us to calculate several statistics at the same time.
df.groupby("state")["salary"].agg([
"count",
"sum",
"mean",
"min",
"max"
])
| state |
|
|
|
|
|
| CDMX |
2 |
53000 |
26500.0 |
25000 |
28000 |
| Jalisco |
2 |
46000 |
23000.0 |
22000 |
24000 |
| Nuevo León |
2 |
62000 |
31000.0 |
30000 |
32000 |
You can also aggregate several numeric variables:
df.groupby("state").agg({
"employees": "sum",
"salary": "mean"
})
| state |
|
|
| CDMX |
270 |
26500.0 |
| Jalisco |
190 |
23000.0 |
| Nuevo León |
370 |
31000.0 |
5. Named aggregations
Named aggregations are particularly useful because we can control the names of the resulting columns.
summary = (
df
.groupby("state", as_index=False)
.agg(
n=("salary", "size"),
total_employees=("employees", "sum"),
average_salary=("salary", "mean"),
minimum_salary=("salary", "min"),
maximum_salary=("salary", "max")
)
)
summary
| 0 |
CDMX |
2 |
270 |
26500.0 |
25000 |
28000 |
| 1 |
Jalisco |
2 |
190 |
23000.0 |
22000 |
24000 |
| 2 |
Nuevo León |
2 |
370 |
31000.0 |
30000 |
32000 |
General syntax:
new_column=("original_column", "aggregation")
For example:
total=("employees", "sum")
6. Group categorical variables
Categorical variables can also be summarized.
For example, count observations by sector:
df.groupby("sector").size()
sector
Private 3
Public 3
dtype: int64
Or:
df.groupby("sector").size().reset_index(name="n")
Count observations for combinations of categories:
df.groupby(["state", "sector"]).size().reset_index(name="n")
| 0 |
CDMX |
Public |
2 |
| 1 |
Jalisco |
Private |
1 |
| 2 |
Jalisco |
Public |
1 |
| 3 |
Nuevo León |
Private |
2 |
7. Count unique categories with nunique()
Suppose we want to know how many different sectors appear in each state.
df.groupby("state")["sector"].nunique()
state
CDMX 1
Jalisco 2
Nuevo León 1
Name: sector, dtype: int64
Or:
df.groupby("state", as_index=False).agg(
sectors=("sector", "nunique")
)
| 0 |
CDMX |
1 |
| 1 |
Jalisco |
2 |
| 2 |
Nuevo León |
1 |
Pivot tables
pivot_table() works similarly to a pivot table in Excel.
General structure:
pd.pivot_table(
df,
index="row_variable",
columns="column_variable",
values="numeric_variable",
aggfunc="aggregation"
)
8. Sum in a pivot table
Sum employees by state and sex:
pd.pivot_table(
df,
index="state",
columns="sex",
values="employees",
aggfunc="sum"
)
| state |
|
|
| CDMX |
150 |
120 |
| Jalisco |
110 |
80 |
| Nuevo León |
170 |
200 |
9. Mean in a pivot table
Average salary by state and sex:
pd.pivot_table(
df,
index="state",
columns="sex",
values="salary",
aggfunc="mean"
)
| state |
|
|
| CDMX |
28000.0 |
25000.0 |
| Jalisco |
24000.0 |
22000.0 |
| Nuevo León |
30000.0 |
32000.0 |
10. Count observations in a pivot table
One way to count observations is:
pd.pivot_table(
df,
index="state",
columns="sex",
values="salary",
aggfunc="count"
)
| state |
|
|
| CDMX |
1 |
1 |
| Jalisco |
1 |
1 |
| Nuevo León |
1 |
1 |
However, for purely categorical counts, pd.crosstab() is often simpler:
pd.crosstab(
df["state"],
df["sex"]
)
| state |
|
|
| CDMX |
1 |
1 |
| Jalisco |
1 |
1 |
| Nuevo León |
1 |
1 |
11. Multiple aggregation functions
A pivot table can calculate several statistics simultaneously.
pd.pivot_table(
df,
index="state",
columns="sex",
values="salary",
aggfunc=["count", "mean", "sum"]
)
| sex |
F |
M |
F |
M |
F |
M |
| CDMX |
1 |
1 |
28000.0 |
25000.0 |
28000 |
25000 |
| Jalisco |
1 |
1 |
24000.0 |
22000.0 |
24000 |
22000 |
| Nuevo León |
1 |
1 |
30000.0 |
32000.0 |
30000 |
32000 |
12. Multiple numeric variables
pd.pivot_table(
df,
index="state",
values=["employees", "salary"],
aggfunc="sum"
)
| state |
|
|
| CDMX |
270 |
53000 |
| Jalisco |
190 |
46000 |
| Nuevo León |
370 |
62000 |
Different aggregations can also be assigned to different variables:
pd.pivot_table(
df,
index="state",
values=["employees", "salary"],
aggfunc={
"employees": "sum",
"salary": "mean"
}
)
| state |
|
|
| CDMX |
270 |
26500.0 |
| Jalisco |
190 |
23000.0 |
| Nuevo León |
370 |
31000.0 |
13. Multiple grouping variables
pd.pivot_table(
df,
index=["state", "sector"],
columns="sex",
values="employees",
aggfunc="sum"
)
| state |
sector |
|
|
| CDMX |
Public |
150.0 |
120.0 |
| Jalisco |
Private |
NaN |
80.0 |
| Public |
110.0 |
NaN |
| Nuevo León |
Private |
170.0 |
200.0 |
14. Replace missing combinations with zero
Pivot tables can generate missing values when a combination does not exist.
Use fill_value:
pd.pivot_table(
df,
index="state",
columns="sector",
values="employees",
aggfunc="sum",
fill_value=0
)
| state |
|
|
| CDMX |
0 |
270 |
| Jalisco |
80 |
110 |
| Nuevo León |
370 |
0 |
15. Add totals
Use margins=True:
pd.pivot_table(
df,
index="state",
columns="sex",
values="employees",
aggfunc="sum",
margins=True,
margins_name="Total"
)
| state |
|
|
|
| CDMX |
150 |
120 |
270 |
| Jalisco |
110 |
80 |
190 |
| Nuevo León |
170 |
200 |
370 |
| Total |
430 |
400 |
830 |
Quick reference
Count rows
df.groupby("category").size()
Count non-null values
df.groupby("category")["variable"].count()
Sum
df.groupby("category")["numeric"].sum()
Mean
df.groupby("category")["numeric"].mean()
Multiple statistics
df.groupby("category")["numeric"].agg([
"count",
"sum",
"mean",
"min",
"max"
])
Recommended named aggregation
df.groupby("category", as_index=False).agg(
n=("numeric", "size"),
total=("numeric", "sum"),
average=("numeric", "mean")
)
Pivot table
pd.pivot_table(
df,
index="category_1",
columns="category_2",
values="numeric",
aggfunc="sum"
)
Categorical frequency table
pd.crosstab(
df["category_1"],
df["category_2"]
)