Mục lục
- Mục tiêu bài học
- Vì sao cần groupby
- Pattern split-apply-combine
df.groupby()trả về cái gì- Aggregation cơ bản
agg()với nhiều hàm- Group theo nhiều cột
- Custom aggregation function
transform()— giữ nguyên shapefilter()— giữ group thoả điều kiệnapply()— linh hoạt nhấtpivot_tablepreviewvalue_counts()vsgroupby().size()- Pitfall thường gặp
- Use case AI/ML
- Code Python tổng hợp
- Bài tập
- Tổng kết Series 1
- Tóm tắt bài
Mục tiêu bài học
- Hiểu pattern split-apply-combine và tại sao nó là công cụ phân tích trung tâm trong Pandas.
- Dùng
df.groupby()với 1 hoặc nhiều cột; biết kết quả làDataFrameGroupBylazy. - Thực hiện aggregation cơ bản:
mean,sum,count,size,nunique; phân biệtcountvàsize. - Dùng
agg(), named aggregation, custom function,transform(),filter(),apply()đúng tình huống. - Preview
pivot_tablevàvalue_countstrong tương quan với groupby. - Biết các pitfall (NaN trong key, warning với cột string trong Pandas 2.x).
Vì sao cần groupby
Rất nhiều câu hỏi data trong thực tế có cùng dạng: "tính một con số nào đó theo từng nhóm". Ví dụ:
- Doanh số trung bình theo từng region.
- Số user theo từng subscription plan.
- Tỉ lệ chuyển đổi theo từng nguồn traffic.
- Click-through rate trung bình theo từng phiên bản A/B test.
Trong SQL bạn viết SELECT region, AVG(sales) FROM ... GROUP BY region. Trong Pandas đó là df.groupby("region")["sales"].mean(). Cùng tư duy, cùng kết quả; nhưng API Pandas còn cho phép nhiều phép biến đổi mà SQL phải vòng vèo qua window function.
Groupby là 1 trong 3 nhóm thao tác bạn dùng đi dùng lại trong mọi notebook EDA: filter row, group + aggregate, và join. Nắm chắc groupby là nắm được phần lớn data wrangling thường ngày.
Pattern split-apply-combine
Hadley Wickham (2011) đặt tên pattern này trong bài báo cùng tên — và đó cũng là cách Pandas tổ chức nội bộ groupby:
- Split: chia DataFrame thành các nhóm con dựa trên giá trị của 1 hay nhiều cột key.
- Apply: áp dụng 1 hàm trên mỗi nhóm — có thể là aggregation (Series → scalar), transformation (Series → Series cùng shape), hoặc filter (Series → boolean).
- Combine: gộp các kết quả con lại thành 1 cấu trúc duy nhất (Series, DataFrame hoặc cùng shape với đầu vào).
Bốn API chính của Pandas tương ứng với 3 kiểu "apply":
agg()/ aggregation method (.mean(),.sum()...) — apply trả scalar.transform()— apply trả Series cùng shape với group đầu vào; kết quả cuối cùng có cùng shape với DataFrame gốc.filter()— apply trảTrue/False, giữ nguyên cả group hoặc loại bỏ cả group.apply()— bao trùm cả 3 loại trên, linh hoạt nhất nhưng cũng chậm nhất.
df.groupby() trả về cái gì
df.groupby("col") KHÔNG trả về DataFrame. Nó trả về 1 đối tượng DataFrameGroupBy "lazy": Pandas mới chỉ ghi nhớ "sẽ chia theo cột này", chưa compute gì cả. Bạn cần gọi tiếp một aggregation/transform/filter để Pandas thực sự làm.
import pandas as pd
df = pd.DataFrame({
"region": ["North", "South", "North", "East", "South", "North"],
"sales": [100, 150, 120, 80, 200, 90],
"units": [10, 12, 11, 7, 18, 9],
})
g = df.groupby("region")
print(g)
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x...>
# Chỉ khi gọi aggregation thì mới ra kết quả
print(g["sales"].mean())
# region
# East 80.000000
# North 103.333333
# South 175.000000
# Name: sales, dtype: float64
DataFrameGroupBy hỗ trợ một số thao tác hữu ích trước khi aggregate:
# Liệt kê các nhóm và index thuộc nhóm đó
print(g.groups)
# {'East': [3], 'North': [0, 2, 5], 'South': [1, 4]}
# Lấy đúng 1 nhóm
print(g.get_group("North"))
# Iterate qua các nhóm — name là giá trị key, group là DataFrame con
for name, group in g:
print(name, "->", len(group), "rows")
Iterate chỉ dùng khi cần làm gì đó phức tạp không vừa với API tổng quát. Trong 99% trường hợp, gọi thẳng agg, transform, hoặc apply đủ và nhanh hơn nhiều.
Aggregation cơ bản
Pandas cung cấp sẵn nhiều aggregation method dưới dạng attribute trực tiếp trên GroupBy:
import pandas as pd
df = pd.DataFrame({
"region": ["North", "South", "North", "East", "South", "North"],
"sales": [100, 150, 120, 80, 200, 90],
"units": [10, 12, 11, 7, 18, 9],
})
g = df.groupby("region")
print(g.mean(numeric_only=True)) # trung bình từng cột số
print(g.sum(numeric_only=True)) # tổng
print(g.count()) # đếm non-NaN, mỗi cột riêng
print(g.size()) # tổng số row mỗi group (kể cả NaN)
print(g.min(numeric_only=True)) # min từng cột số
print(g.max(numeric_only=True)) # max
print(g.std(numeric_only=True)) # std mẫu
print(g.var(numeric_only=True)) # variance
print(g.median(numeric_only=True)) # trung vị
print(g["region"].first()) # giá trị đầu mỗi group
print(g["region"].last()) # giá trị cuối
print(g["sales"].nunique()) # số giá trị duy nhất
Phân biệt 2 cái dễ nhầm:
count()trả về DataFrame: đếm non-NaN riêng cho từng cột. Nếu cột có NaN, kết quả 2 cột có thể khác nhau.size()trả về Series: tổng số row mỗi group, kể cả row có NaN.
Tham số numeric_only=True bỏ qua các cột không phải số; trong Pandas 2.x nếu để mặc định và có cột string, một số phép như mean sẽ raise TypeError hoặc warning. Để cho code rõ ràng, nên truyền tường minh.
agg() với nhiều hàm
agg() (alias: aggregate()) là chiếc API "đa năng" cho phép tính nhiều thống kê cùng lúc.
import pandas as pd
df = pd.DataFrame({
"region": ["North", "South", "North", "East", "South", "North"],
"sales": [100, 150, 120, 80, 200, 90],
"units": [10, 12, 11, 7, 18, 9],
})
# 1) Một cột, nhiều aggregation
print(df.groupby("region")["sales"].agg(["mean", "max", "min", "std"]))
# mean max min std
# region
# East 80.0 80 80 NaN
# North 103.33 120 90 15.275252
# South 175.0 200 150 35.355339
# 2) Per-column aggregation với dict
print(df.groupby("region").agg({"sales": "mean", "units": "sum"}))
# sales units
# region
# East 80.0 7
# North 103.33 30
# South 175.0 30
# 3) Named aggregation (Pandas 0.25+) — KHUYẾN NGHỊ
print(
df.groupby("region").agg(
avg_sales=("sales", "mean"),
max_sales=("sales", "max"),
total_units=("units", "sum"),
)
)
# avg_sales max_sales total_units
# region
# East 80.00 80 7
# North 103.33 120 30
# South 175.00 200 30
Cú pháp named aggregation đặc biệt tiện vì kết quả ra DataFrame với cột được đặt tên rõ ràng — code dễ đọc hơn, và không sinh ra MultiIndex cột (vốn khá khó dùng tiếp).
Bạn cũng có thể truyền function (không phải string) vào agg: g["sales"].agg(np.mean), g["sales"].agg(lambda s: s.quantile(0.9))...
Group theo nhiều cột
Truyền list các cột vào groupby để chia theo tổ hợp nhiều cột:
import pandas as pd
df = pd.DataFrame({
"region": ["North", "North", "North", "South", "South", "South"],
"month": ["Jan", "Feb", "Jan", "Jan", "Feb", "Feb"],
"sales": [100, 120, 90, 150, 200, 180],
})
# Kết quả có MultiIndex (region, month)
print(df.groupby(["region", "month"])["sales"].sum())
# region month
# North Feb 120
# Jan 190
# South Feb 380
# Jan 150
# Name: sales, dtype: int64
# as_index=False để giữ cột phẳng (không MultiIndex)
print(df.groupby(["region", "month"], as_index=False)["sales"].sum())
# region month sales
# 0 North Feb 120
# 1 North Jan 190
# 2 South Feb 380
# 3 South Jan 150
MultiIndex result chuẩn hơn về mặt cấu trúc (mỗi level là 1 key) nhưng đôi khi gây khó khi chain với các API khác. as_index=False hoặc .reset_index() để có DataFrame "phẳng" — thường tiện hơn khi cần export, join hay đem vẽ chart.
Custom aggregation function
Khi cần hàm không có sẵn, truyền function bất kỳ nhận 1 Series và trả 1 scalar:
import pandas as pd
df = pd.DataFrame({
"region": ["North", "South", "North", "East", "South", "North"],
"sales": [100, 150, 120, 80, 200, 90],
})
# Tầm dao động (range) trong mỗi nhóm
print(df.groupby("region")["sales"].agg(lambda s: s.max() - s.min()))
# region
# East 0
# North 30
# South 50
# Name: sales, dtype: int64
# Tỉ lệ top-1 trên total trong nhóm
def top_share(s: pd.Series) -> float:
return s.max() / s.sum()
print(df.groupby("region")["sales"].agg(top_share))
Function dạng này được gọi 1 lần cho mỗi group. Mỗi lần input là 1 pd.Series tương ứng với 1 cột × 1 group; output bắt buộc phải là 1 scalar (số, string, hoặc None).
transform() — giữ nguyên shape
transform() khác agg() ở chỗ kết quả CÙNG SHAPE với DataFrame gốc — phù hợp khi bạn muốn gắn 1 giá trị tính theo group vào TỪNG ROW.
import pandas as pd
df = pd.DataFrame({
"region": ["North", "South", "North", "East", "South", "North"],
"sales": [100, 150, 120, 80, 200, 90],
})
# 1) Trung bình theo region, gắn vào từng row
df["region_avg"] = df.groupby("region")["sales"].transform("mean")
print(df)
# region sales region_avg
# 0 North 100 103.333333
# 1 South 150 175.000000
# 2 North 120 103.333333
# 3 East 80 80.000000
# 4 South 200 175.000000
# 5 North 90 103.333333
# 2) Z-score TRONG mỗi nhóm (chuẩn hoá theo group)
df["sales_z"] = df.groupby("region")["sales"].transform(
lambda s: (s - s.mean()) / s.std()
)
# 3) Điền NaN của cột "sales" bằng MEAN CỦA CÙNG REGION
# df["sales"] = df.groupby("region")["sales"].transform(lambda s: s.fillna(s.mean()))
Use case transform phổ biến trong ML / phân tích:
- Per-group normalization: z-score theo region, ngành, cohort.
- Per-group imputation: fill NaN bằng mean/median của cùng nhóm (đã thấy ở bài missing data).
- Feature engineering: lệch của 1 giá trị so với trung bình nhóm, tỉ lệ trên tổng nhóm.
filter() — giữ group thoả điều kiện
filter() nhận 1 function trả về True/False trên mỗi group; nếu True thì giữ TOÀN BỘ group, ngược lại bỏ. Kết quả là DataFrame gốc đã thu hẹp.
import pandas as pd
df = pd.DataFrame({
"city": ["HN", "HN", "HCM", "DN", "DN", "HCM", "HN"],
"sales": [100, 110, 90, 70, 80, 120, 95],
})
# Chỉ giữ city có >= 3 record
print(df.groupby("city").filter(lambda g: len(g) >= 3))
# city sales
# 0 HN 100
# 1 HN 110
# 6 HN 95
# Chỉ giữ city có tổng sales > 200
print(df.groupby("city").filter(lambda g: g["sales"].sum() > 200))
Đây là 1 trong những use case sạch đẹp nhất của split-apply-combine: thay vì phải viết 2 bước (compute size mỗi nhóm, join lại, filter), bạn diễn đạt thẳng "loại các group nhỏ".
apply() — linh hoạt nhất
apply() truyền nguyên 1 DataFrame con của mỗi group vào function — function có thể trả về scalar, Series, hoặc DataFrame; Pandas sẽ tự ghép lại. Linh hoạt nhất, nhưng cũng chậm nhất vì không tối ưu được nội bộ như agg/transform.
import pandas as pd
df = pd.DataFrame({
"region": ["North", "South", "North", "East", "South", "North"],
"sales": [100, 150, 120, 80, 200, 90],
"units": [10, 12, 11, 7, 18, 9],
})
# Trả về Series: top 2 sales trong mỗi region
print(df.groupby("region").apply(lambda g: g.nlargest(2, "sales")))
# Trả về DataFrame nhỏ tự thiết kế cho từng group
def summarize(g: pd.DataFrame) -> pd.Series:
return pd.Series({
"n": len(g),
"total": g["sales"].sum(),
"avg_price": g["sales"].sum() / g["units"].sum(),
})
print(df.groupby("region").apply(summarize))
Quy tắc thực hành: ưu tiên agg / transform / filter nếu vừa; chỉ rơi xuống apply khi cấu trúc output không vừa cả 3. Trên dataset lớn (hàng triệu row), apply có thể chậm gấp nhiều lần và là điểm nghẽn phổ biến trong notebook EDA.
pivot_table preview
pivot_table là một dạng "view" của groupby — nó cũng là split-apply-combine, nhưng kết quả được trải ra dạng bảng chéo (cross-tab) với 1 trục là index, 1 trục là columns:
import pandas as pd
df = pd.DataFrame({
"region": ["North", "North", "South", "South", "East"],
"month": ["Jan", "Feb", "Jan", "Feb", "Jan"],
"sales": [100, 120, 150, 200, 80],
})
print(df.pivot_table(values="sales", index="region", columns="month", aggfunc="sum"))
# month Feb Jan
# region
# East NaN 80.0
# North 120.0 100.0
# South 200.0 150.0
Tương đương với df.groupby(["region", "month"])["sales"].sum().unstack("month") — cùng dữ liệu, layout khác. pivot_table thuận khi cần báo cáo dạng bảng cho người đọc; groupby + agg thuận khi tiếp tục pipeline tính toán.
value_counts() vs groupby().size()
2 cách viết cùng làm 1 việc: đếm số row theo từng giá trị của 1 cột.
import pandas as pd
df = pd.DataFrame({"city": ["HN", "HN", "HCM", "DN", "HCM", "HN"]})
print(df["city"].value_counts())
# city
# HN 3
# HCM 2
# DN 1
# Name: count, dtype: int64
print(df.groupby("city").size())
# city
# DN 1
# HCM 2
# HN 3
# dtype: int64
Khác biệt nhỏ:
value_counts()mặc định sort theo count giảm dần;groupby().size()mặc định sort theo key.value_counts(normalize=True)trả về tỉ lệ (proportion) — rất hay dùng cho EDA.value_countsbỏ NaN mặc định; truyềndropna=Falseđể giữ.
Pitfall thường gặp
- NaN trong cột key bị loại: mặc định
groupbybỏ qua row có NaN ở cột key. Nếu muốn giữ, dùngdf.groupby("col", dropna=False)(Pandas 1.1+). - Cột string trong aggregation số: Pandas 2.x raise warning hoặc lỗi khi gọi
.mean()trên DataFrame có cột string. Dùngnumeric_only=Truehoặc chọn cột tường minh trước:g["sales"].mean(). - MultiIndex sau group nhiều cột: kết quả có index 2 level; dùng
reset_index()hoặcas_index=Falseđể có DataFrame phẳng cho dễ join. - Nhầm
countvớisize:countbỏ NaN per-column,sizeđếm tổng row. Nếu có NaN, 2 con số sẽ khác. - Lạm dụng
apply:applykhông tận dụng được vectorization; với dataset hàng triệu row, đổi sangagg/transformthường nhanh gấp nhiều lần. - Sort không ổn định:
groupbymặc định sort theo key (sort=True). Tắt bằngsort=Falsenếu muốn giữ thứ tự xuất hiện — đôi khi nhanh hơn trên dataset lớn. - Quên là
groupbylazy: gáng = df.groupby(...)không hề tốn chi phí compute; chỉ khi gọi tiếp aggregation mới chạy. Có thể tái dùnggnhiều lần.
Use case AI/ML
- Aggregate feature per user/session: trung bình số click, tổng thời gian session, max amount mua hàng — rồi gắn vào table user để làm feature cho model.
- Group by class label để tính class statistics: mean / std từng feature trong từng class — giúp hiểu xem feature có phân biệt được class không (intuition cho linear model, hoặc xây Naive Bayes thủ công).
- Time series resampling (preview):
df.resample("D")["sales"].sum()là groupby chuyên cho time, group các record theo bucket thời gian. - Feature engineering theo group:
transform("mean")để tạo cột "trung bình spending theo region của user này" — rất phổ biến trong các bài tabular ML có cột categorical cardinality cao. - A/B test analysis:
df.groupby("variant").agg(n=("user_id", "nunique"), conv=("converted", "mean"))ra ngay bảng size + conversion rate cho từng variant. - Class balance check:
df["target"].value_counts(normalize=True)hoặcdf.groupby("target").size()để biết tỉ lệ class trước khi train classifier.
Code Python tổng hợp
import numpy as np
import pandas as pd
# Dataset giả lập: đơn hàng theo region và category
rng = np.random.default_rng(42)
df = pd.DataFrame({
"order_id": range(1, 13),
"region": ["North", "South", "North", "East",
"South", "North", "East", "South",
"North", "South", "East", "North"],
"category": ["A", "B", "A", "A", "B", "B",
"A", "B", "A", "A", "B", "B"],
"amount": rng.integers(50, 500, size=12),
"units": rng.integers(1, 10, size=12),
})
# --- 1) Aggregation cơ bản theo region ---
print("Doanh số trung bình theo region:")
print(df.groupby("region")["amount"].mean().round(2))
# --- 2) Nhiều agg với named aggregation ---
print("\nThống kê theo region:")
print(
df.groupby("region").agg(
n_orders=("order_id", "count"),
total_amount=("amount", "sum"),
avg_amount=("amount", "mean"),
max_amount=("amount", "max"),
total_units=("units", "sum"),
).round(2)
)
# --- 3) Group theo (region, category) — MultiIndex ---
print("\nTổng doanh số theo (region, category):")
print(df.groupby(["region", "category"])["amount"].sum())
# --- 4) Transform: z-score amount TRONG mỗi region ---
df["amount_z_in_region"] = df.groupby("region")["amount"].transform(
lambda s: (s - s.mean()) / s.std()
)
print("\nDataFrame với z-score per region:")
print(df[["region", "amount", "amount_z_in_region"]].round(3))
# --- 5) Filter: chỉ giữ region có >= 4 đơn ---
print("\nChỉ giữ region có >= 4 đơn:")
print(df.groupby("region").filter(lambda g: len(g) >= 4))
# --- 6) Custom agg: tầm dao động (range) amount ---
print("\nTầm dao động amount theo region:")
print(df.groupby("region")["amount"].agg(lambda s: s.max() - s.min()))
Bài tập
- Tạo DataFrame đơn hàng với 3 cột
category,price,quantity(~20 row, có lặp category). Tínhrevenue = price * quantity, rồi tính doanh thu trung bình theo từng category bằnggroupby("category")["revenue"].mean(). - Tạo DataFrame sinh viên với cột
class_idvàstudent_id. Đếm số sinh viên mỗi lớp bằnggroupby("class_id").size(). Đếm lại bằngvalue_counts()và so sánh. - Với DataFrame có cột
region,customer_id,spending: tìm khách hàng có total spending cao nhất trong MỖI region (gợi ý:df.groupby(["region", "customer_id"])["spending"].sum().groupby(level=0).idxmax()). - Z-score normalization theo group: tạo cột
score_zbằngdf.groupby("class_id")["score"].transform(lambda s: (s - s.mean()) / s.std()). Kiểm tra:df.groupby("class_id")["score_z"].agg(["mean", "std"]).round(3)— mean ≈ 0 và std ≈ 1 trong mỗi nhóm. - (Mở rộng) Dùng
pivot_tabletạo bảng doanh số theoregion×month. So sánh code vớigroupby+unstack— kết quả phải giống.
Tổng kết Series 1
Đây là bài cuối của Series 1. Nhìn lại 41 bài, khối kiến thức nền tảng đã đi qua gồm 4 nhóm:
- Python cơ bản — kiểu dữ liệu, control flow, function, list / dict comprehension, file I/O, virtualenv. Đây là ngôn ngữ làm việc chính.
- Toán nền tảng — đại số tuyến tính (vector, matrix, dot product, eigen), giải tích (đạo hàm, gradient, gradient descent ở B27), xác suất - thống kê (mean / variance / std, phân phối, Bayes).
- NumPy — array, dtype, indexing, broadcasting, vectorization, các phép linalg cơ bản. Là backend tính toán cho gần như mọi thư viện ML/DL.
- Pandas — Series / DataFrame, đọc - ghi CSV / Parquet, indexing (
loc/iloc), filtering, sort, missing data, và groupby trong bài này.
Series 2 (Machine Learning Foundations) sẽ dùng lại tất cả: sklearn xây trên NumPy và Pandas; gradient descent (B27) là cốt lõi training của hầu hết model; mean / std (B22) cho normalization và scaling; xử lý missing data (B40) cho preprocessing pipeline; groupby (B41) cho feature engineering theo nhóm.
Không cần thuộc lòng từng API. Quay lại tra cứu bài tương ứng khi cần. Mỗi bài Series 2 sẽ giả định bạn đã quen với cấu trúc DataFrame, np.ndarray, và biết tự viết 1 pipeline đơn giản load - clean - feature - split.
Tóm tắt bài
- Groupby = pattern split-apply-combine: chia theo key, áp dụng hàm trên mỗi nhóm, gộp kết quả.
df.groupby("col")lazy, trả vềDataFrameGroupBy; phải gọi tiếp aggregation/transform/filter để compute.- Aggregation cơ bản:
mean,sum,count(non-NaN per column),size(tổng row per group),nunique,min,max,std,var,median. agg()đa năng; named aggregation (avg=("x","mean")) là cú pháp được khuyến nghị vì output có cột đặt tên rõ ràng.transform()giữ shape — dùng cho z-score per group, fill NaN per group, feature engineering.filter()giữ hoặc loại nguyên cả group theo điều kiện.apply()linh hoạt nhất nhưng chậm — chỉ dùng khi 3 cái trên không vừa.pivot_tablelà groupby +unstack;value_counts()là alias tiện chogroupby().size()(kèm sort desc và normalize).- Pitfall: NaN trong key bị loại (dùng
dropna=False), warning với cột string (dùngnumeric_only=True), nhầmcountvớisize.
- Pandas User Guide - Group by: split-apply-combine
- Pandas Docs - DataFrame.groupby
- Pandas Docs - DataFrameGroupBy.agg
- Pandas Docs - DataFrameGroupBy.transform
- Pandas Docs - DataFrameGroupBy.filter
- Pandas Docs - DataFrameGroupBy.apply
- Pandas Docs - pivot_table
- Pandas Docs - Series.value_counts
- Wickham (2011) - The Split-Apply-Combine Strategy for Data Analysis, Journal of Statistical Software 40(1)
