ทำไม GroupBy ถึงสำคัญที่สุดใน pandas?
ถ้าคุณทำงานกับข้อมูล คุณแทบจะหนีไม่พ้นการ "จับกลุ่ม" ข้อมูลแล้ววิเคราะห์ ไม่ว่าจะเป็นยอดขายรวมแต่ละภาค ค่าเฉลี่ยเงินเดือนตามแผนก หรือจำนวนลูกค้าตามช่วงอายุ — ทั้งหมดนี้คือหน้าที่ของ groupby()
แต่ปัญหาที่คนจำนวนมากเจอ (รวมถึงตัวผมเองตอนเริ่มใช้ pandas ใหม่ๆ) คือ: เมื่อไหร่ควรใช้ agg() เมื่อไหร่ควรใช้ transform() แล้ว apply() ล่ะ ต่างกันยังไง? สามตัวนี้ดูคล้ายกันมาก แต่ output กับกรณีใช้งานต่างกันอย่างสิ้นเชิง ถ้าเลือกผิดตัว โค้ดจะช้าลง หรือแย่กว่านั้นคือได้ผลลัพธ์ผิดโดยไม่รู้ตัว
งั้นมาเจาะลึกกันเลย — ตั้งแต่พื้นฐานจนถึงเทคนิคขั้นสูง อัปเดตสำหรับ pandas 3.0 (มกราคม 2026) พร้อมโค้ดตัวอย่างที่ก๊อปไปใช้ได้ทันที
หลักการ Split-Apply-Combine ที่ต้องเข้าใจก่อน
ก่อนจะไปไหนต่อ ต้องเข้าใจแนวคิดนี้ก่อนครับ ทุก groupby() operation ทำงานตามรูปแบบ Split-Apply-Combine ซึ่งมี 3 ขั้นตอน:
- Split (แยก) — แบ่ง DataFrame ออกเป็นกลุ่มตามค่าที่ไม่ซ้ำกันในคอลัมน์ที่กำหนด
- Apply (ประยุกต์ใช้) — นำฟังก์ชันไปทำงานกับแต่ละกลุ่ม
- Combine (รวม) — รวมผลลัพธ์กลับเป็น DataFrame หรือ Series ใหม่
ขั้นตอน Apply นี่แหละที่มีหลายรูปแบบ — และนั่นคือเหตุผลที่ pandas แยกเป็น agg(), transform(), apply(), และ filter() ฟังดูเยอะ แต่จริงๆ แล้วพอเข้าใจแต่ละตัวก็ตรงไปตรงมาครับ
มาเริ่มจากข้อมูลตัวอย่างที่จะใช้ตลอดบทความ:
import pandas as pd
import numpy as np
# สร้าง DataFrame ตัวอย่าง — ข้อมูลยอดขายร้านค้า
df = pd.DataFrame({
"store": ["กรุงเทพ", "กรุงเทพ", "กรุงเทพ", "เชียงใหม่", "เชียงใหม่", "เชียงใหม่", "ภูเก็ต", "ภูเก็ต"],
"product": ["A", "B", "A", "A", "B", "C", "A", "C"],
"sales": [150, 200, 180, 90, 120, 60, 300, 250],
"quantity": [10, 15, 12, 8, 10, 5, 20, 18],
"date": pd.to_datetime(["2026-01-15", "2026-01-16", "2026-02-10",
"2026-01-20", "2026-02-05", "2026-02-15",
"2026-01-10", "2026-01-25"])
})
print(df)
agg() — รวบรวมค่าสรุปสำหรับแต่ละกลุ่ม
agg() (ย่อจาก aggregate) เป็นตัวเลือกแรกที่ควรนึกถึงเมื่อต้องการ สรุปข้อมูลแต่ละกลุ่มให้เหลือค่าเดียว เช่น ผลรวม ค่าเฉลี่ย ค่าสูงสุด หรือจำนวนนับ พูดตรงๆ ก็คือ ถ้าอยากได้ตัวเลข summary — นึกถึง agg() ก่อนเลย
การใช้งานพื้นฐาน
# ยอดขายรวมแต่ละร้าน
df.groupby("store")["sales"].agg("sum")
# store
# กรุงเทพ 530
# เชียงใหม่ 270
# ภูเก็ต 550
# หลายฟังก์ชันพร้อมกัน
df.groupby("store")["sales"].agg(["sum", "mean", "max", "count"])
Named Aggregation — ตั้งชื่อคอลัมน์ผลลัพธ์
อันนี้ต้องบอกว่าเป็นฟีเจอร์ที่ชอบมาก ทำให้โค้ดอ่านง่ายขึ้นเยอะเลย — ใช้ keyword arguments เพื่อกำหนดชื่อคอลัมน์ผลลัพธ์เองได้:
# Named Aggregation — อ่านง่ายและชัดเจน
result = df.groupby("store").agg(
total_sales=("sales", "sum"),
avg_sales=("sales", "mean"),
total_qty=("quantity", "sum"),
num_transactions=("sales", "count"),
)
print(result)
# total_sales avg_sales total_qty num_transactions
# store
# กรุงเทพ 530 176.67 37 3
# เชียงใหม่ 270 90.00 23 3
# ภูเก็ต 550 275.00 38 2
ใช้ฟังก์ชันต่างกันในแต่ละคอลัมน์
# คำนวณ sum สำหรับ sales แต่ mean สำหรับ quantity
df.groupby("store").agg({"sales": "sum", "quantity": "mean"})
Custom Aggregation Function
บางทีเราก็ต้องการคำนวณอะไรที่ built-in function ทำไม่ได้ ก็เขียนเองได้เลย:
# ฟังก์ชันที่คำนวณ range (ค่าสูงสุด - ค่าต่ำสุด)
df.groupby("store")["sales"].agg(lambda x: x.max() - x.min())
# store
# กรุงเทพ 50
# เชียงใหม่ 60
# ภูเก็ต 50
สิ่งสำคัญที่ต้องจำ: agg() ทำงานกับแต่ละคอลัมน์แยกกัน ไม่สามารถคำนวณข้ามคอลัมน์ได้ (เช่น ไม่สามารถใช้ค่าจากทั้ง sales และ quantity ในฟังก์ชันเดียวกัน) ถ้าต้องการข้ามคอลัมน์ ให้ใช้ apply() แทน — เดี๋ยวจะพูดถึงด้านล่างครับ
transform() — คำนวณแล้วใส่ค่ากลับทุกแถว
พูดตามตรง transform() คือพระเอกตัวจริงที่หลายคนมองข้ามไป ความพิเศษของมันคือ: ผลลัพธ์มีจำนวนแถวเท่ากับ DataFrame ต้นฉบับเป๊ะ โดยค่าที่คำนวณจะถูก "broadcast" กลับไปยังทุกแถวในกลุ่มนั้นๆ
พูดง่ายๆ ก็คือ — ถ้ากลุ่ม "กรุงเทพ" มี 3 แถว แล้วค่าเฉลี่ย sales คือ 176.67 ทั้ง 3 แถวจะได้รับค่า 176.67 กลับไป
การใช้งานพื้นฐาน
# เพิ่มคอลัมน์ค่าเฉลี่ย sales ของแต่ละร้าน
df["avg_store_sales"] = df.groupby("store")["sales"].transform("mean")
print(df[["store", "sales", "avg_store_sales"]])
# store sales avg_store_sales
# 0 กรุงเทพ 150 176.666667
# 1 กรุงเทพ 200 176.666667
# 2 กรุงเทพ 180 176.666667
# 3 เชียงใหม่ 90 90.000000
# 4 เชียงใหม่ 120 90.000000
# 5 เชียงใหม่ 60 90.000000
# 6 ภูเก็ต 300 275.000000
# 7 ภูเก็ต 250 275.000000
กรณีใช้งานจริง: คำนวณสัดส่วนภายในกลุ่ม
อันนี้ใช้บ่อยมากในงานจริง สมมติอยากรู้ว่าแต่ละรายการคิดเป็นกี่เปอร์เซ็นต์ของยอดทั้งร้าน:
# สัดส่วน sales ของแต่ละรายการเทียบกับยอดรวมร้าน
df["pct_of_store"] = df["sales"] / df.groupby("store")["sales"].transform("sum")
print(df[["store", "product", "sales", "pct_of_store"]])
# store product sales pct_of_store
# 0 กรุงเทพ A 150 0.283019
# 1 กรุงเทพ B 200 0.377358
# 2 กรุงเทพ A 180 0.339623
# 3 เชียงใหม่ A 90 0.333333
# 4 เชียงใหม่ B 120 0.444444
# 5 เชียงใหม่ C 60 0.222222
# 6 ภูเก็ต A 300 0.545455
# 7 ภูเก็ต C 250 0.454545
กรณีใช้งานจริง: Z-Score Normalization ภายในกลุ่ม
# Standardize sales ภายในแต่ละร้าน (Z-Score)
df["z_score"] = df.groupby("store")["sales"].transform(
lambda x: (x - x.mean()) / x.std()
)
print(df[["store", "sales", "z_score"]])
กรณีใช้งานจริง: กรองด้วยเงื่อนไขของกลุ่ม
ทริคเล็กๆ ที่คนไม่ค่อยรู้ — ใช้ transform ร่วมกับ boolean mask เพื่อกรอง DataFrame ได้:
# เลือกเฉพาะร้านที่มียอดขายรวมมากกว่า 400
mask = df.groupby("store")["sales"].transform("sum") > 400
df_filtered = df[mask]
print(df_filtered)
# เหลือเฉพาะ กรุงเทพ (530) และ ภูเก็ต (550)
apply() — ยืดหยุ่นสูงสุด แต่ใช้อย่างระวัง
apply() รับ DataFrame ทั้งกลุ่ม เข้ามาเป็น argument (ไม่ใช่แค่ Series ทีละคอลัมน์เหมือน agg/transform) ทำให้สามารถคำนวณข้ามคอลัมน์ได้อย่างอิสระ แต่ข้อแลกเปลี่ยนที่ต้องยอมรับคือ มันช้ากว่า agg กับ transform พอสมควร
คำนวณข้ามคอลัมน์
# คำนวณ weighted average price (ราคาเฉลี่ยถ่วงน้ำหนัก) ของแต่ละร้าน
def weighted_avg_price(group):
return (group["sales"] * group["quantity"]).sum() / group["quantity"].sum()
result = df.groupby("store").apply(weighted_avg_price)
print(result)
# store
# กรุงเทพ 174.864865
# เชียงใหม่ 91.304348
# ภูเก็ต 276.315789
Return DataFrame จาก apply()
# เลือก 2 รายการที่ขายดีที่สุดของแต่ละร้าน
def top_n(group, n=2):
return group.nlargest(n, "sales")
result = df.groupby("store").apply(top_n, n=2)
print(result[["store", "product", "sales"]])
เมื่อไหร่ที่ apply() ดีกว่า agg()?
จากประสบการณ์ส่วนตัว ผมจะใช้ apply() ต่อเมื่อ:
- ต้องคำนวณข้ามหลายคอลัมน์ (เช่น weighted average ข้างบน)
- ต้อง return DataFrame ที่มีหลายแถวต่อกลุ่ม (เช่น top N)
- ลอจิกซับซ้อนเกินกว่าจะใช้ built-in aggregation function ได้
กฎง่ายๆ: ถ้าทำด้วย agg() หรือ transform() ได้ ให้ใช้มันก่อนเสมอ จะทั้งเร็วกว่าและปลอดภัยกว่าครับ
filter() — คัดเลือกกลุ่มทั้งกลุ่ม
นอกจาก 3 ตัวหลักแล้ว ยังมี filter() ที่หลายคนลืมไปเลย ตัวนี้ใช้ คัดเลือกกลุ่มทั้งกลุ่ม ตามเงื่อนไข — ถ้ากลุ่มไหนผ่านก็เก็บทุกแถว ถ้าไม่ผ่านก็ตัดทิ้งยกกลุ่มเลย:
# เก็บเฉพาะร้านที่มียอดขายเฉลี่ยมากกว่า 100
df_filtered = df.groupby("store").filter(lambda x: x["sales"].mean() > 100)
print(df_filtered)
# เหลือเฉพาะ กรุงเทพ (avg 176.67) และ ภูเก็ต (avg 275)
สังเกตนะครับว่า filter() คืน DataFrame ที่มีแถวเหมือนเดิม (ไม่ย่อและไม่เปลี่ยนรูปร่าง) แค่ตัดกลุ่มที่ไม่ผ่านเงื่อนไขออกไป
เปรียบเทียบ agg() vs transform() vs apply() vs filter()
ตรงนี้สำคัญ เพื่อให้เห็นภาพชัดเจนขึ้น ลองดูตารางเปรียบเทียบ:
| คุณสมบัติ | agg() | transform() | apply() | filter() |
|---|---|---|---|---|
| รูปร่าง Output | ย่อ (1 แถว/กลุ่ม) | เท่าเดิม | ยืดหยุ่น | เท่าเดิม (ลบกลุ่ม) |
| Input ของฟังก์ชัน | Series (ต่อคอลัมน์) | Series (ต่อคอลัมน์) | DataFrame (ทั้งกลุ่ม) | DataFrame (ทั้งกลุ่ม) |
| หลายฟังก์ชัน | ได้ | ทีละตัว | ได้ | ทีละตัว |
| ข้ามคอลัมน์ | ไม่ได้ | ไม่ได้ | ได้ | ได้ |
| ประสิทธิภาพ | เร็ว | เร็ว | ช้ากว่า | ปานกลาง |
| เหมาะกับ | สรุปค่า | Feature engineering | ลอจิกซับซ้อน | คัดกรองกลุ่ม |
ผมแนะนำให้จำตารางนี้ไว้ครับ จะช่วยตัดสินใจได้เร็วขึ้นมากว่าควรเลือกตัวไหน
GroupBy หลายคอลัมน์ — Multi-level Grouping
ในงานจริงๆ แทบจะไม่ค่อยได้ group แค่คอลัมน์เดียวหรอกครับ ส่วนใหญ่ต้อง group ด้วยหลายคอลัมน์พร้อมกัน:
# Group ตามร้านและสินค้า
result = df.groupby(["store", "product"]).agg(
total_sales=("sales", "sum"),
avg_qty=("quantity", "mean")
)
print(result)
# total_sales avg_qty
# store product
# กรุงเทพ A 330 11.0
# B 200 15.0
# เชียงใหม่ A 90 8.0
# B 120 10.0
# C 60 5.0
# ภูเก็ต A 300 20.0
# C 250 18.0
ใช้ as_index=False เพื่อได้ DataFrame แบน
ไม่ชอบ MultiIndex? (ส่วนตัวผมก็ไม่ค่อยชอบเหมือนกัน) ใช้ as_index=False ได้เลย:
# ไม่ต้องการ MultiIndex? ใช้ as_index=False
result = df.groupby(["store", "product"], as_index=False).agg(
total_sales=("sales", "sum")
)
print(result)
# store product total_sales
# 0 กรุงเทพ A 330
# 1 กรุงเทพ B 200
# 2 เชียงใหม่ A 90
# ...
อัปเดต pandas 3.0: GroupBy มีอะไรใหม่?
pandas 3.0 ออกมาตั้งแต่มกราคม 2026 และมีการปรับปรุง groupby() หลายจุดที่น่าสนใจ มาดูกัน:
1. Unobserved Groups ทำงานถูกต้องแล้ว
เรื่องนี้เป็น pain point มานานครับ ใน pandas เวอร์ชันก่อนหน้า เมื่อใช้ observed=False กับ Categorical data แล้ว groupby หลายคอลัมน์ กลุ่มที่ไม่มีข้อมูลจะคืนค่า NaN แทนที่จะเป็นค่าที่ถูกต้อง ตอนนี้แก้แล้ว:
# pandas 3.0 — unobserved groups ทำงานถูกต้อง
df["store"] = df["store"].astype("category")
result = df.groupby("store", observed=False)["sales"].sum()
# กลุ่มที่ไม่มีข้อมูลจะได้ค่า 0 (ไม่ใช่ NaN อีกต่อไป)
2. value_counts() เรียงลำดับตาม input
เมื่อใช้ sort=False ตอนนี้ value_counts() จะรักษาลำดับตาม input แทนที่จะเรียงตาม label ซึ่งสอดคล้องกับ Series.value_counts() แล้ว ดีใจที่แก้ตรงนี้มา
3. Copy-on-Write เป็นค่าเริ่มต้น
ใครที่เคยเจอ SettingWithCopyWarning แล้วปวดหัว (คงเกือบทุกคนล่ะ) ข่าวดีครับ pandas 3.0 ใช้ Copy-on-Write เป็นค่าเริ่มต้นแล้ว ไม่ต้องกังวลเรื่องนี้อีกต่อไปเมื่อทำงานกับผลลัพธ์จาก groupby
4. String operations เร็วขึ้น 5-10 เท่า
ด้วย PyArrow-backed str dtype ใหม่ การ groupby ด้วยคอลัมน์ข้อความจะเร็วขึ้นอย่างเห็นได้ชัด ถ้าข้อมูลของคุณมีคอลัมน์ string เยอะ อัปเดตแล้วจะรู้สึกถึงความแตกต่างแน่นอน
เทคนิคขั้นสูง: resample() กับ Time Series GroupBy
ตรงนี้เป็นอะไรที่ผมใช้บ่อยมากในงานจริง เมื่อข้อมูลมี datetime index คุณสามารถใช้ resample() ซึ่งเป็น groupby ที่ออกแบบมาสำหรับ time series โดยเฉพาะ:
# ยอดขายรวมรายเดือนของแต่ละร้าน
monthly = df.set_index("date").groupby("store").resample("ME")["sales"].sum()
print(monthly)
# หรือใช้ Grouper สำหรับ groupby ปกติ
result = df.groupby(["store", pd.Grouper(key="date", freq="ME")])["sales"].sum()
print(result)
Performance Tips สำหรับ GroupBy ที่เร็วขึ้น
เคล็ดลับเหล่านี้ช่วยเร่งความเร็ว groupby ได้จริงๆ โดยเฉพาะกับ DataFrame ขนาดใหญ่:
1. ใช้ sort=False เมื่อไม่ต้องการเรียงลำดับ
# ปิด sorting — เร็วขึ้นเพราะไม่ต้อง sort ผลลัพธ์
df.groupby("store", sort=False)["sales"].sum()
2. ใช้ Category dtype สำหรับคอลัมน์ที่ group
# แปลงเป็น category ก่อน groupby — เร็วขึ้นโดยเฉพาะกับข้อมูลใหญ่
df["store"] = df["store"].astype("category")
df.groupby("store")["sales"].sum()
3. หลีกเลี่ยง apply() กับ UDF ถ้าเป็นไปได้
อันนี้เน้นย้ำอีกทีครับ ดูตัวอย่างความแตกต่าง:
# ❌ ช้า: ใช้ apply กับ lambda
df.groupby("store")["sales"].apply(lambda x: x.sum())
# ✅ เร็ว: ใช้ built-in method
df.groupby("store")["sales"].sum()
4. เลือกคอลัมน์ก่อน groupby
เรื่องเล็กๆ แต่ส่งผลต่อ performance เยอะกว่าที่คิด:
# ❌ group ทั้ง DataFrame แล้วค่อยเลือก
df.groupby("store").sum()["sales"]
# ✅ เลือกคอลัมน์ก่อน — เร็วกว่า
df.groupby("store")["sales"].sum()
ตัวอย่างจริง: วิเคราะห์ยอดขายด้วย GroupBy Pipeline
มาดูตัวอย่างแบบครบวงจรกัน — ใช้ groupby หลายขั้นตอนต่อเนื่องกันเพื่อวิเคราะห์ข้อมูลยอดขาย:
import pandas as pd
import numpy as np
# สร้างข้อมูลจำลองขนาดใหญ่ขึ้น
np.random.seed(42)
n = 10000
data = pd.DataFrame({
"store": np.random.choice(["กรุงเทพ", "เชียงใหม่", "ภูเก็ต", "พัทยา", "หาดใหญ่"], n),
"category": np.random.choice(["อาหาร", "เครื่องดื่ม", "ของใช้", "เสื้อผ้า"], n),
"sales": np.random.uniform(50, 500, n).round(2),
"quantity": np.random.randint(1, 50, n),
"date": pd.date_range("2026-01-01", periods=n, freq="h"),
})
# ขั้นตอนที่ 1: สรุปภาพรวมแต่ละร้าน (agg)
summary = data.groupby("store").agg(
total_revenue=("sales", "sum"),
avg_order_value=("sales", "mean"),
total_items=("quantity", "sum"),
order_count=("sales", "count"),
).round(2)
print("=== สรุปแต่ละร้าน ===")
print(summary)
# ขั้นตอนที่ 2: เพิ่ม % ส่วนแบ่งยอดขายเทียบกับยอดรวม (transform)
data["revenue_share_pct"] = (
data["sales"] / data.groupby("store")["sales"].transform("sum") * 100
).round(4)
# ขั้นตอนที่ 3: หาสินค้า top 3 ของแต่ละร้าน (apply)
top3 = data.groupby("store").apply(
lambda g: g.nlargest(3, "sales")[["category", "sales"]]
)
print("\n=== Top 3 ยอดขายสูงสุดแต่ละร้าน ===")
print(top3)
# ขั้นตอนที่ 4: เก็บเฉพาะร้านที่มี avg > 250 (filter)
high_performers = data.groupby("store").filter(
lambda x: x["sales"].mean() > 250
)
print(f"\nร้านที่ avg > 250: {high_performers['store'].unique()}")
คำถามที่พบบ่อย (FAQ)
groupby() กับ pivot_table() ต่างกันอย่างไร?
คำถามนี้เจอบ่อยมาก ทั้งคู่ใช้รวบรวมข้อมูล แต่ต่างกันที่รูปร่าง output ครับ — groupby() ให้ตาราง "ยาว" (long format) ส่วน pivot_table() ให้ตาราง "กว้าง" (wide format) ที่คล้ายกับ Pivot Table ใน Excel จริงๆ แล้ว pivot_table() เรียก groupby() ภายในแล้วตามด้วย unstack() ดังนั้น groupby().agg().unstack() จะให้ผลลัพธ์เดียวกันแต่เร็วกว่าเล็กน้อย
ทำไม transform() เร็วกว่า apply() ทั้งที่ดูคล้ายกัน?
เหตุผลหลักคือ transform() รู้ล่วงหน้าว่า output จะมีรูปร่างเดียวกับ input จึงสามารถจองหน่วยความจำล่วงหน้าและใช้ vectorized operations ได้ ส่วน apply() ต้อง "เดา" ว่า output จะเป็นรูปร่างแบบไหน ทำให้มี overhead เพิ่มเติม โดยทั่วไปแล้ว transform() เร็วกว่าประมาณ 2 เท่า ซึ่งในข้อมูลขนาดใหญ่ ความแตกต่างนี้มีผลมากเลยครับ
จะใช้ groupby กับ missing values (NaN) ได้อย่างไร?
โดยค่าเริ่มต้น pandas จะ ข้ามค่า NaN ในคอลัมน์ที่ group (เหมือนกับ dropna=True) ถ้าต้องการรวม NaN เป็นอีกกลุ่มหนึ่ง ให้ใส่ dropna=False:
df.groupby("store", dropna=False)["sales"].sum()
มีวิธี groupby แบบ rolling window ไหม?
ได้ครับ ใช้ .groupby().rolling() สำหรับคำนวณ rolling statistics แยกตามกลุ่ม มีประโยชน์มากกับข้อมูล time series:
# Rolling mean 3 รายการล่าสุดของแต่ละร้าน
df.sort_values("date").groupby("store")["sales"].rolling(3).mean()
pandas 3.0 เปลี่ยนอะไรใน groupby บ้าง?
สรุปสั้นๆ: กลุ่มที่ไม่มีข้อมูล (unobserved groups) ใน Categorical data ตอนนี้ได้รับค่า identity ที่ถูกต้อง (เช่น 0 สำหรับ sum, 1 สำหรับ prod) แทนที่จะเป็น NaN, value_counts(sort=False) รักษาลำดับ input, และ Copy-on-Write เป็นค่าเริ่มต้นทำให้ไม่ต้องกังวลเรื่อง SettingWithCopyWarning อีกต่อไป ถ้ายังใช้ pandas 2.x อยู่ อัปเกรดได้เลยครับ ดีขึ้นหลายจุด