groupby — Split-Apply-Combine 패턴
이 토픽을 마치면
pandas groupby()의 내부 동작을 Split-Apply-Combine 패턴으로 설명할 수 있고, 실무에서 자주 쓰는 그룹별 집계, 변환, 필터링 패턴을 익힙니다.
왜 그룹별 연산이 필요한가
"전체 평균"은 df["score"].mean()으로 한 줄입니다. 하지만 실무 질문은 대부분 이런 형태입니다:
- 부서별 평균 연봉은?
- 월별 매출 합계는?
- 카테고리별 가장 많이 팔린 상품은?
"~별"이 붙는 순간, 그룹별 연산이 필요합니다. 이것이 groupby()의 역할입니다.
Split-Apply-Combine 패턴
Hadley Wickham이 2011년에 명명한 이 패턴은, 데이터 분석에서 가장 자주 반복되는 사고 구조입니다.
원본 데이터
┌────────┬──────┬───────┐
│ dept │ name │ score │
├────────┼──────┼───────┤
│ Sales │ Alice│ 85 │
│ Sales │ Bob │ 90 │
│ Dev │ Carol│ 95 │
│ Dev │ Dave │ 88 │
│ HR │ Eve │ 78 │
└────────┴──────┴───────┘
1. SPLIT — 부서별로 나누기
Sales: [85, 90]
Dev: [95, 88]
HR: [78]
2. APPLY — 각 그룹에 함수 적용 (예: 평균)
Sales: 87.5
Dev: 91.5
HR: 78.0
3. COMBINE — 결과를 하나로 합치기
┌────────┬───────┐
│ dept │ score │
├────────┼───────┤
│ Dev │ 91.5 │
│ HR │ 78.0 │
│ Sales │ 87.5 │
└────────┴───────┘pandas에서는 이 세 단계가 한 줄입니다:
df.groupby("dept")["score"].mean()기본 사용법
import pandas as pd
df = pd.DataFrame({ "dept": ["Sales", "Sales", "Dev", "Dev", "HR"], "name": ["Alice", "Bob", "Carol", "Dave", "Eve"], "score": [85, 90, 95, 88, 78], "salary": [50000, 52000, 70000, 68000, 45000]})
# 부서별 점수 평균df.groupby("dept")["score"].mean()# dept# Dev 91.5# HR 78.0# Sales 87.5
# 부서별 급여 합계df.groupby("dept")["salary"].sum()# dept# Dev 138000# HR 45000# Sales 102000GroupBy 객체란
grouped = df.groupby("dept")print(type(grouped)) # <class 'pandas.core.groupby.DataFrameGroupBy'>groupby()는 즉시 계산하지 않습니다. 지연 연산(lazy evaluation) — 어떤 함수(mean, sum 등)를 적용할 때 비로소 계산이 실행됩니다. 이것은 대용량 데이터에서 불필요한 연산을 방지합니다.
그룹 내용 확인
for name, group in df.groupby("dept"): print(f"\n--- {name} ---") print(group)
# --- Dev ---# dept name score salary# 2 Dev Carol 95 70000# 3 Dev Dave 88 68000# --- HR ---# ...여러 집계 함수
하나의 컬럼, 여러 함수
df.groupby("dept")["score"].agg(["mean", "min", "max", "count"])# mean min max count# dept# Dev 91.5 88 95 2# HR 78.0 78 78 1# Sales 87.5 85 90 2컬럼별 다른 함수
df.groupby("dept").agg( avg_score=("score", "mean"), total_salary=("salary", "sum"), headcount=("name", "count"))# avg_score total_salary headcount# dept# Dev 91.5 138000 2# HR 78.0 45000 1# Sales 87.5 102000 2Named aggregation(컬럼명=("원래컬럼", "함수"))은 결과의 컬럼 이름까지 한 번에 지정할 수 있어서 가장 깔끔한 방식입니다.
여러 컬럼으로 그룹화
sales = pd.DataFrame({ "region": ["East", "East", "West", "West", "East", "West"], "category": ["A", "B", "A", "B", "A", "A"], "revenue": [100, 200, 150, 250, 120, 180]})
sales.groupby(["region", "category"])["revenue"].sum()# region category# East A 220# B 200# West A 330# B 250리스트로 여러 컬럼을 넘기면 계층적 인덱스(MultiIndex) 가 생깁니다.
# MultiIndex를 일반 컬럼으로result = sales.groupby(["region", "category"])["revenue"].sum().reset_index()# region category revenue# 0 East A 220# 1 East B 200# 2 West A 330# 3 West B 250reset_index()는 그룹 키를 인덱스에서 일반 컬럼으로 되돌립니다. 후속 작업(시각화, 저장 등)에서는 이 형태가 편합니다.
transform — 그룹별 변환
agg()는 그룹별로 하나의 값을 반환합니다 (5행 → 3행). transform()은 원본과 같은 크기의 결과를 반환합니다 (5행 → 5행).
# 부서별 평균을 원본 데이터에 붙이기df["dept_avg"] = df.groupby("dept")["score"].transform("mean")print(df)# dept name score salary dept_avg# 0 Sales Alice 85 50000 87.5# 1 Sales Bob 90 52000 87.5# 2 Dev Carol 95 70000 91.5# 3 Dev Dave 88 68000 91.5# 4 HR Eve 78 45000 78.0각 행에 자기 부서의 평균이 붙습니다. 이것으로 부서 평균 대비 점수를 계산할 수 있습니다:
df["vs_avg"] = df["score"] - df["dept_avg"]# Alice: 85 - 87.5 = -2.5 (부서 평균 이하)# Carol: 95 - 91.5 = +3.5 (부서 평균 이상)filter — 그룹별 필터링
조건을 만족하는 그룹 전체를 남기거나 제거합니다.
# 인원이 2명 이상인 부서만df.groupby("dept").filter(lambda g: len(g) >= 2)# dept name score salary# 0 Sales Alice 85 50000# 1 Sales Bob 90 52000# 2 Dev Carol 95 70000# 3 Dev Dave 88 68000HR(1명)이 통째로 제거되었습니다. 개별 행이 아니라 그룹 단위로 필터링하는 것이 filter()의 핵심입니다.
실전 패턴 — 월별 매출 분석
orders = pd.DataFrame({ "date": pd.to_datetime([ "2026-01-05", "2026-01-15", "2026-02-03", "2026-02-20", "2026-03-10", "2026-03-25" ]), "product": ["Widget", "Gadget", "Widget", "Gadget", "Widget", "Widget"], "amount": [1200, 800, 1500, 900, 1100, 1300]})
# 월별 매출 합계monthly = orders.groupby(orders["date"].dt.to_period("M"))["amount"].sum()print(monthly)# date# 2026-01 2000# 2026-02 2400# 2026-03 2400
# 상품별 월 평균 매출orders.groupby("product").agg( total=("amount", "sum"), avg=("amount", "mean"), count=("amount", "count"))# total avg count# Gadget 1700 850.0 2# Widget 5100 1275.0 4날짜 데이터에서 .dt.to_period("M")으로 월 단위 그룹을 만드는 것은 매우 자주 쓰는 패턴입니다.
핵심 정리
| 메서드 | 동작 | 결과 크기 |
|---|---|---|
agg() | 그룹별 집계 (sum, mean, count...) | 그룹 수만큼 (줄어듦) |
transform() | 그룹별 변환 (원본 크기 유지) | 원본과 동일 |
filter() | 조건 만족하는 그룹 전체 유지 | 원본 이하 |
groupby()는 pandas에서 가장 강력하면서도 가장 자주 쓰이는 기능입니다. Split-Apply-Combine 패턴을 머릿속에 그릴 수 있으면, 복잡한 집계도 "어떤 컬럼으로 나누고(Split), 무슨 함수를 적용하고(Apply), 결과를 어떤 형태로 합칠지(Combine)" 세 단계로 자연스럽게 분해할 수 있습니다.